Document-based app doesn't restore documents with non-file URLs - cocoa

I have an application based on NSDocument with an NSDocumentController subclass. My NSDocument works with both file URLs and URLs with a custom scheme which use a web service.
I handle much of the loading and saving using custom code, including -saveToURL:ofType:forSaveOperation:completionHandler:. +autosavesInPlace returns YES.
The problem I'm having: documents with the custom URL scheme aren't restored on startup. Documents with the file URL scheme are – both regular documents saved to files, and untitled documents which are autosaved.
After leaving open server-based documents and quitting the app, no NSDocument methods appear to be called on restart. In particular, none of the four initializers is called:
–init
–initWithContentsOfURL:ofType:error:
–initForURL:withContentsOfURL:ofType:error:
–initWithType:error:
The NSDocumentController method -reopenDocumentForURL:withContentsOfURL:display:completionHandler: is not called either.
How and when are documents' restorable state encoded? How and when are they decoded?

NSDocument is responsible for encoding its restorable state in -encodeRestorableStateWithCoder:, and NSDocumentController is responsible for decoding documents' restorable state and reopening the documents in +restoreWindowWithIdentifier:state:completionHandler:. Refer to the helpful comments in NSDocumentRestoration.h.
When NSDocument encodes the URL, it appears to use the bookmark methods of NSURL. The problem is that these methods only work with file-system URLs. (It's possible non-file URLs will encode, but they will not properly decode.)
To fix the problem, override the encoding of NSDocument instances which use the custom scheme, and likewise, the decoding of those documents.
NSDocument subclass:
- (void) encodeRestorableStateWithCoder:(NSCoder *) coder {
if ([self.fileURL.scheme isEqualToString:#"customscheme"])
[coder encodeObject:self.fileURL forKey:#"MyDocumentAutoreopenURL"];
else
[super encodeRestorableStateWithCoder:coder];
}
NSDocumentController subclass:
+ (void) restoreWindowWithIdentifier:(NSString *) identifier
state:(NSCoder *) state
completionHandler:(void (^)(NSWindow *, NSError *)) completionHandler {
NSURL *autoreopenURL = [state decodeObjectForKey:#"MyDocumentAutoreopenURL"];
if (autoreopenURL) {
[[self sharedDocumentController]
reopenDocumentForURL:autoreopenURL
withContentsOfURL:autoreopenURL
display:NO
completionHandler:^(NSDocument *document, BOOL documentWasAlreadyOpen, NSError *error) {
NSWindow *resultWindow = nil;
if (!documentWasAlreadyOpen) {
if (![[document windowControllers] count])
[document makeWindowControllers];
if (1 == document.windowControllers.count)
resultWindow = [[document.windowControllers objectAtIndex:0] window];
else {
for (NSWindowController *wc in document.windowControllers)
if ([wc.window.identifier isEqual:identifier]) {
resultWindow = wc.window;
break;
}
}
}
completionHandler(resultWindow, error);
}
];
} else
[super restoreWindowWithIdentifier:identifier
state:state
completionHandler:completionHandler];
}
The behavior or the completion handler follows from Apple's method comment in NSDocumentRestoration.h and should be roughly the same as super's.

Window state encoding is enabled by two methods on NSWindow. Calling setRestorable: on the window marks it as one that can be saved and restored on relaunch, and then calling setRestorationClass: lets you specify a class that will handle recreating that saved window.
By default, AppKit sets NSDocumentController as the restoration class of windows controlled by NSDocument objects. The actual restoration is done by calling the method +restoreWindowWithIdentifier:state:completionHandler:, defined by the NSWindowRestoration protocol. For documents, NSDocumentController implements that method and recreates the NSDocument object based on the state encoded in the NSCoder instance passed into the method.
So, theoretically, if you were to subclass NSDocumentController and override that method, that would give you an opportunity to restore documents saved by the state restoration mechanism. However, as far as I know, the keys used by NSDocumentController to store state are not documented anywhere, so I don't think there would be a reliable way to restore directly from the state that NSDocumentController stores itself.
To support this, you would probably need to encode the entire state for the document yourself, by implementing -encodeRestorableStateWithCoder: on the NSWindow being encoded, and/or implement the window:willEncodeRestorableState: delegate method for the window. Both of those methods pass you an NSCoder instance you can use to encode your state. That's where you would encode your custom-schemed URL, along with any other associated data you need to save/restore your state. You would then decode that state in the restoreWindowWithIdentifier:state:completionHandler: method.
Since you'll have some documents with regular file URLs and some with your custom URLs, I would approach that by creating a separate class responsible for decoding the document state, and set that as the restoration class only for your documents with custom URLs, leaving NSDocumentController to handle the documents withe file URLs for you.

Related

Under what conditions can an NSWindowController's document change?

My app observes the document property of its NSWindowController and performs some UI setup when it is set. Once it has been set, it would be difficult to rebuild the UI (for internal reasons) as the result of a change.
Once an NSWindowController has set its document property to an opened document, under what conditions will the system ever change that property to a new NSDocument instance (i.e., will the document ever be swapped out)? I've never observed it happening, but I can imagine features like versions or iCloud syncing causing the window controller's document to get swapped for a new document. However, the documentation on the NSWindowController lifecycle doesn't seem to touch on the issue.
It doesn't change once it has been set. NSWindowController gets its document by addWindowController method. Every new/opened document creates it's own windowController. Instance of document doesn't change with iCloud or revertChanges. It is up to you how to synchronise document with its views (redrawing).
/* Create the user interface for this document, but don't show it yet.
The default implementation of this method invokes [self windowNibName],
creates a new window controller using the resulting nib name (if it is not nil),
specifying this document as the nib file's owner, and then invokes [self addWindowController:theNewWindowController] to attach it. You can override
this method to use a custom subclass of NSWindowController or to create more
than one window controller right away. NSDocumentController invokes this method
when creating or opening new documents.
*/
// e.g. override
- (void)makeWindowControllers
{
if ([[self windowControllers] count] == 0) {
MainWindowController *controller = [[MainWindowController alloc] init];
[self addWindowController:controller];
}
}

Setting up multiple NSWindowController objects and NSDocument

I'm new to the NSDocument architecture and am looking to set up multiple windows (and hence multiple NSWindowController objects) for a single document.
From what I understand, NSDocument was really created to work with a single window, and it seems that the ability to have multiple windows was shoehorned in later. For example, it seems that the NSDocument should always be the file's owner for any window's NIB files. But what if I wanted to separate the window controllers from the document?
For example, in the NSDocument subclass I am currently using the code:
- (void)makeWindowControllers {
[self setMyWindowController1:[[WindowControllerType1 alloc] initWithWindowNibName:#"MyWindow" owner:self]];
[self addWindowController:[self MyWindowController1]];
}
But the NIB file "MyWindow"'s file owner is set to the NSWindowController subclass (WindowControllerType1), NOT my NSDocument subclass. In this case, whenever I look to get the document by using [[NSDocumentController sharedDocumentController] currentDocument], this ALWAYS returns nil.
I figure this can be rectified if I set the NIB file's owner to the NSDocument subclass, but then all of my outlet links break, and I'm not sure how to link to the NSWindowController subclass (WindowControllerType1), as the typical course of action (as far as I can tell) is to make the NSDocument a window controller delegate as well, which I would like to avoid!
Any suggestions?
EDIT:
Let me clarify and add some new information. I am aware of Apple's position on using the WindowController's document property. However, as I plan of having a larger number of nested NSViews in each window, I want to avoid passing the document through a large chain of views in order to accomplish this.
The issue is not necessarily this chain. It is mostly that when the [[NSDocumentController sharedDocumentController] currentDocument] is ALWAYS nil, none of the "for free" features of NSDocument seem to work, such as undo/redo. This is the major issue that I need to resolve.
From what I understand, NSDocument was really created to work with a single window, and it seems that the ability to have multiple windows was shoehorned in later.
No, makeWindowControllers is available in OS X v10.0 and later.
But what if I wanted to separate the window controllers from the document?
The window controller owns the NIB.
Any suggestions?
Do
[self setMyWindowController1:[[WindowControllerType1 alloc] initWithWindowNibName:#"MyWindow"]].
NSWindowController has a property document which is set by addWindowController:.
Use document property of NSWindowController instead of currentDocument.

Where to get managed object context in NSPersistentDocument?

It seems there's spotty information out there for using Core-data with Document based apps. I have a window controller that runs a modal window in the current document. The user enters data into a text field, which creates a mutable array of strings, which I want to use to create model objects (for a many-to-many relationship), and then save them to the core-data stack. This is the method I have in the modal window's controller.
- (IBAction)saveContext:(id)sender {
if ([tagsArray count] != 0) {
int objectcount;
for (objectcount = 0; objectcount < [tagsArray count]; objectcount ++){
Tag *singleTag = (Tag *) [NSEntityDescription insertNewObjectForEntityForName:#"Tag" inManagedObjectContext:self.managedObjectContext];
singleTag.tagname = [tagsArray objectAtIndex:objectcount];
singleTag.video = selectedFile;
NSLog(#"Tagnames %#",singleTag.tagname);
}
}
[NSApp stopModalWithCode:NSOKButton];
[self.window close];
}
Ok the compiler isn't happy with self.managedObjectContext. Understandably so, since this class doesn't have a context. The way I understand it, with a document based app you want to use only one MOC. What I don't understand is how to access the document's MOC. Apple's docs are a little unclear.
Getting a Managed Object Context
In OS X:
In an single-coordinator applications, you can get the application’s context directly from the application delegate.
In document-based applications, you can get the context directly from the document instance.
I'm embarrassed to say I don't know what this means. How do I get the context from the document instance? Is it some sort of global variable? Any help is greatly appreciated.
When you create your Modal Window pass it the documents managedObjectContext to use.
So maybe have a property in the controller class for the modal window and set that modalWindow.moc=self.managedObjectContext prior to calling modalWindow.show or whatever you are using. Assuming self is your NSPersistentDocument subclass.
You must used the documents existing MOC, don't create a new one (you can but you don't want to go there).
The documents MOC is your definitive access point for adding objects to your Core Data store.
NSPersistentDocument has a managedObjectContext method to get its managed object context:
NSManagedObjectContext *context = [yourPersistentDocument managedObjectContext];

Is -makeWindowControllers the best place to initialize an NSPersistentDocument?

When loading an existing document using NSPersistentDocument, as part of initialization I'd like to prepare some content:
NSFetchRequest *req = [NSFetchRequest fetchRequestWithEntityName:#"DocumentRoot"];
NSArray *results = [self.managedObjectContext executeFetchRequest:req error:NULL];
if (results.count) self._docRoot = [results objectAtIndex:0];
When I put this code in -init, the fetch request doesn't return any results.
I encountered this problem while refactoring the view-controller components from my NSPersistentDocument subclass to a new NSWindowController subclass. I used to handle this initialization in -windowControllerDidLoadNib:, but that isn't called anymore.
If I move the code from -init to -makeWindowControllers I get the results I expect. Is -makeWindowControllers really the right place to prepare content like this?
Based on the responses I've gotten I think I'm doing the right thing, so here's my answer to my own question.
If you're using the Core Data stack provided by NSPersistentDocument, you can not use Core Data in -init.
Instead, you should:
Put the document-initialization code directly in -windowControllerDidLoadNib: – or if you use a custom NSWindowController subclass, in -makeWindowControllers.
You may also abstract the document-initialization code into a helper method with some unique name like -setUpDocument, and call that method from -makeWindowControllers/-windowControllerDidLoadNib: instead.
If you're using a plain NSDocument, or you're setting up the Core Data stack on your own, you can set up the document model in -init.
From this question and your related question about NSArrayControllers, I'm gathering that you're doing something like this:
- (void)makeWindowControllers
{
MyWindowController* wc = [[[MyWindowController alloc] initWithWindowNibName: [self windowNibName]] autorelease];
[self addWindowController: wc];
}
When you do this, -windowControllerDidLoadNib: won't be called, because the NSDocument object isn't the Nib's owner if you init that way. If you look at NSDocument.h you'll see the following comment (see added emphasis):
/* Create the user interface for this document, but don't show it yet. The
default implementation of this method invokes [self windowNibName],
creates a new window controller using the resulting nib name (if it is
not nil), **specifying this document as the nib file's owner**, and then
invokes [self addWindowController:theNewWindowController] to attach it.
You can override this method to use a custom subclass of
NSWindowController or to create more than one window controller right
away. NSDocumentController invokes this method when creating or opening
new documents.
*/
- (void)makeWindowControllers;
If you, instead, do this:
- (void)makeWindowControllers
{
MyWindowController* wc = [[[MyWindowController alloc] initWithWindowNibName: [self windowNibName] owner: self] autorelease];
[self addWindowController: wc];
}
I believe you'll find that -windowControllerDidLoadNib: is called again. That may not help you, if you have a good reason for that Nib's owner to not be the NSDocument, but that's why -windowControllerDidLoadNib: isn't being called, and what you can do to get that behavior back. That's almost certainly a better place to be doing fetches than in init, which likely happens before all the necessary CoreData support stuff is in place. So that's one option.
If the code is not called from init that is because your document is being initialized elsewhere such as initWithContentsOfURL:ofType:error:, initForURL:withContentsOfURL:ofType:error:, initWithType:error: or initWithCoder: makeWindowControllers is not for setting up your data. Try implementing all of the above initializers and log to see which is getting called.

In NSWindowController subclass, [self document] returns null

I am using a custom subclass of NSDocument and a custom subclass of NSWindowController. The problem is that I cannot reference my custom document from my custom window controller.
In IB, in the TKDocument NIB I have File's Owner set to TKWindowController.
In my TKDocument subclass I have:
- (void) makeWindowControllers {
TKWindowController *controller = [[TKWindowController alloc] init];
[self addWindowController:controller];
}
Then in my TKWindowController subclass I overrode setDocument to make sure it was being called:
- (void) setDocument(NSDocument *) document {
NSLog(#"setDocument:%#", document);
[super setDocument:document];
}
and then (again in TKWindowController) my action which references the document itself:
- (IBAction) plotClicked:(id) sender {
TKDocument *doc = [self document];
NSLog(#"plotClicked %#", doc);
}
The NSLog in setDocument outputs the string returned by my [TKDocument description] override as I'd expect; I only put it there to see if it was being called. However, doc in plotClicked is null.
What might I have done wrong?
EDIT: I believe the problem is to do with NIBs. My Document has its own NIB with File's Owner set to the custom controller as mentioned above. The plotClicked action is fired from a menu item in MainMenu.xib. I believe it's hitting a new instance of the controller which isn't associated with the current, active document.
So, how do I link the two? My question is really this: How do I obtain a handle to the current active document (or its windowcontroller) from MainMenu.xib?
Thanks
My Document has its own NIB with File's Owner set to the custom controller as mentioned above.
The File's Owner of a document nib should be the document. Consider that suspect #1.
The plotClicked action is fired from a menu item in MainMenu.xib. I believe it's hitting a new instance of the controller which isn't associated with the current, active document.
Did you put a window controller inside your main menu nib? If not, then that isn't the problem, since you must have wired up your plotClicked: menu item to the First Responder, and the window controller and its document will be in the responder chain.
If you did, then there's the solution: delete the window controller from the MainMenu nib and hook up your menu item to the First Responder, so that the action message goes down the responder chain, which will enable it to hit the document or window controller.
How do I obtain a handle to …?
The only Handles on the Mac come from Carbon; those Handles do not exist in Cocoa.
init is not a designated initializer of NSWindowController. You want one of these: – initWithWindow:, – initWithWindowNibName:, – initWithWindowNibName:owner:, or – initWithWindowNibPath:owner:.
Also, from the docs:
In your class’s initialization method,
be sure to invoke on super either one
of the initWithWindowNibName:...
initializers or the initWithWindow:
initializer. Which one depends on
whether the window object originates
in a nib file or is programmatically
created.

Resources