Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 857
paulmelnikow Avatar asked Dec 12 '12 21:12

paulmelnikow


2 Answers

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.

like image 199
paulmelnikow Avatar answered Sep 25 '22 15:09

paulmelnikow


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.

like image 20
Brian Webster Avatar answered Sep 21 '22 15:09

Brian Webster