Re: Swift with NSDocument and Revert To... browse all versions.


Quincey Morris
 

On Jul 12, 2018, at 18:22 , Bill Pitcher <bill@...> wrote:

Firstly I’ve uploaded a very cut down project which shows the problem.
https://www.ilike.co.nz/downloads/testDocument.zip

Well, you’re Doing It Wrong™, but it’s not entirely your fault. This is one of NSDocument’s big messes that we’ve been stuck with for the last 10 years or so.

What’s actually going wrong is that there are multiple document objects involved. Starting with your “latest edit” document (+ its controller hierarchy) A, when you call up the version browser, it displays A on the left and creates a new document B to display on the right. Assuming you choose to revert to B, what you see is B animate back to the original display position of A, while the rest of the version browser UI fades away.

That’s why you see the reversion happen correctly, seemingly. What happens next, though, is that document B is *closed*, since it was only a temporary adjunct needed for the version browser UI. When its window closes, A’s window is revealed underneath, and you see the content un-revert to the latest data that you had in A when this all started.

Why discard B and keep A, when B has the correct data? Because the reversion mechanism doesn’t want to leave you with a different instance of your NSDocument subclass (B) from the one you started with (A), since you might have code that knows and depends on its actual object pointer. It would break existing code if the pointer for the active document suddenly changed.

Instead, what actually happens is that (by default) your document’s reading method — read(from:ofType:) — is invoked anew *in A* but with the data from the reverted version store, and it causes A’s data model to be replaced with a new one that has the reverted data.

That would all be fine, except that your design doesn’t have any machinery to propagate this last data model change to your controller hierarchy. You have code to do it when the window controller’s “document” property changes, but it isn’t changing for document A here, so nothing happens.

There are a couple of things you should do differently:

— I *strongly* urge you not to override NSWindowController’s “document” property. NSWindowController is old-school, and even if the property is changed KVO-compliantly, there’s no guarantee it will actually go through the setter. (OTOH, I don’t think this has any impact on the current problem.)

— I *strongly* urge you not to make your view controller’s “representedObject” refer to the document, but rather to the root object of your data model.

— You need a way of getting notifications to the controller objects when the data model changes, rather than when the document changes. There are multiple approaches, but the one I usually use is to have the window controller observe its own “document.model” keypath (it would be “document.contents” in your sample project) to maintain its own “model” derived property KVO compliantly. Then I have the view controllers observe the window’s “model” property, and update their UI from that change notification. Normally, I don’t bother with the “representedObject” property (not least because it’s untyped and requires casting to be useful), but repeat the “model” property in the view controller if it’s useful to store it there too.

Note that there are other NSDocument override points that can intercept a revert operation earlier, if that’s useful. You might do that if recreating the entire document via read(from:ofType:) is expensive or has side effects, and you have a cheaper or safer way of doing the reversion, or if you want to be able to tell the difference between an “open” read(from:ofType:) and a “revert” one.

Join cocoa@apple-dev.groups.io to automatically receive all group messages.