Do I need to override revertToSaved ?

I'm creating a MacOS document-based app in Swift. When running the app, if I make modifications to the document, and then select "Revert to Saved", I get a dialog asking me if I want to revert the document, losing current changes, but if I click on "Revert", nothing happens. The document keeps its changes.


Furthermore, after clicking Revert, the document behaves as if there are no changes -- the titlebar icon is not grey; and the window closes without asking to save changes (I'm not using Autosave). It's like ChangeCount or isDocumentEdited have been reset.


If I don't select Revert, then closing the document's window with unsaved changes brings a dialog asking to save.
Apple's documentation for .revertToSaved reads as follows:

The default implementation of this method presents an alert dialog giving the user the opportunity to cancel the operation. If the user chooses to continue, the method ensures that any editor registered using the Cocoa Bindings

NSEditorRegistration
informal protocol has discarded its changes and then invokes
revert(toContentsOf:ofType:)
. If that returns
false
, the method presents the error to the user in an document-modal alert dialog.


This suggests to me that it should work without needing to be overridden.


Any idea why this is happening? Do I need to override the revert method to load the data back from disk?
I've not yet sorted out Undo for the app, which might be relevant. But I am using ChangeCount to check whether edits have been made.

Accepted Reply

hi,


after reading John's reply and then your code above, i'm sorry now to say that the advice i was offering was only correct if you were following MVC/iOS paradigm. i assumed you were. my apologies for not fully finishing the code.


John is right on three fronts:


(1) MacOS is becoming more iOS-like; the notions of an NSViewController even being in the responder chain and having viewWillAppear() and viewDidAppear() were only introduced maybe 4 years ago.


(2) your code is setting up its views to have direct references to the things they draw (along with some parameters). when the NSDocument is reverted, you're not telling the views that their references must be updated first before the views are redrawn.


(3) you need to set up references for the NSViews outside of viewDidAppear(). in fact, i don't see that you even want to touch viewDidAppear(). then you can call that code both upon startup and when revert happens.


so, based on your question and seeing your most recent code above, John's asking you to rewrite this way:



override func viewDidLoad() { 
  super.viewDidLoad() 
  NotificationCenter.default.addObserver(self, selector: #selector(handleDocumentReverted), name: .documentReverted, object: nil) 
  loadViewParameters() 
} 


func loadViewParameters() { 
  let theThumbnailSize = CGSize(width: 200, height: 200) 
  self.thePDFView?.document = document?.thePDFDocument 
  // self.thePDFView?.autoScales = true 
  self.thePDFView?.displayMode = theDisplayMode 
  self.thePDFView?.displaysAsBook = bookState 


  self.theThumbnailView?.pdfView = self.thePDFView 
  self.theThumbnailView?.thumbnailSize = theThumbnailSize 
} 


@objc func handleDocumentReverted(_ note: Notification) { 
  // i mentioned that you may not have to check directly in some cases
  // whether you're responding to the right document's revert in these 2 lines:
  // let sendingDocument = note.object as? Document 
  // guard sendingDocument == document else { return }
  loadViewParameters()
  thePDFView.setNeedsDisplay(thePDFView.bounds) 
  theThumbnailView.setNeedsDisplay(theThumbnailView.bounds) 
}



hope that helps,

DMG

Replies

"revert(toContentsOf:ofType:)" should invoke whatever "read" method you implemented in your NSDocument subclass, the same one that was invoked when you opened the document originally.


That method presumably reads the document contents again, and updates some instance variables in the document. You have to make sure that those changes are propagated elsewhere (e.g. other things in the document subclass that depend on that data, plus state in window controllers and view controllers).


You can find out whether the "read" method is being invoked by setting a breakpoint. After that, it's up to you to handle the data propagation issue yourself. You may need different code for the "open" and "revert" cases.


Or, you can override "revert(toContentsOf:ofType:)", and doing something more custom at a higher level.

benwiggy,


i think you do need to have your own revert method. a simple one would look like this in Swift:


override func revert(toContentsOf url: URL, ofType typeName: String) throws {

  do {
    let data = try Data(contentsOf: url)
    try read(from: data, ofType:typeName)
  }

}


you might want to catch some errors here, perhaps, rather than simply let the throw happen normally from the try statements:


  1. if Data(contentsOf:) throws, i'm not sure exactly what that would mean -- maybe a file has been moved out from underneath you and your document url is no longer valid, so you could catch that here, throw your own error type, and no changes will have been made to what you have.
  2. if the read(from: ofType:) throws, that should be already handled by your read() method.


although i have a few tweaks to this to send out a few notifications that the document has been reverted (which makes me rethink why i needed to worry about that), this basic code structure works for me.


hope that helps,

DMG

Thanks. I tried your function, which is a partial success, as it keeps the "isEdited" flag on, but doesn't reload the document.


I tried adding breakpoints, as Polyphonic suggested, and it does get past the read line, but the document is still not restored.


I'll try a few other things.

Do I have to 'reload the view' somehow? Maybe the data is being refreshed, but the display is not.

I don't know all of Apple's APIs. But of the ones that I have encountered, I would rate NSDocument as the single most complicated. The NSDocument architecure is based on the developer subclassing various Apple classes. There are a number of very specific, and very delicate flows of execution in NSDocument and all of your derived classes must behave the way that Apple expects them to. It has quite a bit of documentation, but I confess that it hasn't really helped me due to the overall level of complexity of the API. Some of the newer behaviors, like revert, are not as well documented.


Furthermore, this architecture has evolved significantly over the years. It has a number of different behaviours that the developer can choose. Which read/write methods are you using? Data, FileWrapper, or URL? Are you autosaving in place? I could go on and on, but that is not a productive course of analysis.


I think if revert isn't working as you expect, then you have probably done something else wrong at a more basic level. You go it working, which is an achievement, but it wasn't done correctly. That is what you need to review. Unfortunately, that is where things get really murky. You have to figure out how the entire architecture works. When is a document created? When is its data read? When is the data displayed? When are the UI elements initialized and setup? How does save work? How does a rename work? What happens when you close the window? How does that, or any UI actions really, relate to the document object?


My suspcion is that you have made assumptions of sequential behaviour and cause/effect. A better approach is to never make such assumptions. Design your code such that your underlying data can change at any time, either by the user via the UI or through some external process. I will tell you that using the NSData read/write methods will make your life much easier. If you are still thinking in terms of data on disk, data in ram, and data in display, then you will have problems. There is only one kind of data, your structured model data. It can be updated via the UI or perhaps independently. It can always be entirely replaced by a new flat data stream. But don't concern yourself with data on disk, where the data is on disk, or how the data is read or especially saved. Therein lies madness.

Yes, you have to arrange for the UI to be updated, somehow. There are automatic mechanisms (such as KVO, bindings or notifications) that you might have designed into your app, otherwise you'll need to force the update.

Thanks. This is where I stumble conceptually, as the revert function is in Document.swift, and the view stuff is obviously in ViewController.swift. But I'm sure I'll find a way.


Is restoreWindow method something to try? Not sure what the parameters would be, though.

I'm using PDFDocument as my data model, so that's under control. I'm familiar with that from python.


I mostly have problems due to the separation of data and view (which I realise is a good thing). How I get the document to control the view, I'm not sure.


Next step is to sort out Undo, and I'm not looking forward to that!

Here's my code:


var viewController: ViewController? {

return windowControllers[0].contentViewController as? ViewController

}


override func revert(toContentsOf url: URL, ofType typeName: String) throws {

do {

try super.revert(toContentsOf: url, ofType: typeName)

let docsview = viewController?.view

docsview?.setNeedsDisplay(docsview!.bounds)

}

}


It still doesn't update the view.

This isn't the right approach, because it violates the MVC design principle. Your data model isn't supposed to know what views are using it.


The question is, how does your ViewController know what data to display when the document is first opened? Whatever it does, you need to do that again after the revert —more or less.

How can the reverttoSaved meothd of NSDocument refresh the view it's in, then?
My ViewController uses:


var document: Document? {

return self.view.window?.windowController?.document as? Document

}


to get the document data.

You've shown two ends of an execution path in your last 2 replies (triggering an update at one end, re-fetching the document data at the other end). Use the debugger to follow the path from one end to the other, until you find the point where it doesn't work.

The code works: it just doesn't do what I want.

I don't think that is the right approach. Technically speaking, in a document-based app, your NSDocument subclass if your data model. But practically speaking, NSDocument is so deeply ingrained in Apple's NSDocument architecture that it really isn't suitable as a data model. You will likely need some other object to manage the data.


Unfortunately, PDFDocument also isn't suitable. You need an object that can interact with the rest of the MVC architecture using KVO, bindings, or notifications, as Polyphonic says (Well, not quite, don't use notifications. Just use bindings. 🙂) PDFDocument is mainly useful for serializing and deserializing the PDF document from NSData. It will give you access to the PDF data, but you will still need some kind of wrapper to interact with said data.


For example, I wrote a little app a few years ago that would edit ePub and PDF files. My edits were pretty basic. I was just changing a few metadata fields like title and author and changing the cover. I could have done those edits directly into the PDFDocument, but that would be adding a lot of complexity and sticking it into the UI flow. I didn't want to do that. Instead, I read the PDF data and extracted the attributes I needed, saving them into NSStrings and an NSImage. Then, in my UI, I just interacted with the strings and the image, which is drop-dead simple. With the data model abstracted like this, my UI flow was identical whether I was editing a PDF or an ePub. Plus, I could easily test my UI with dummy data, keeping the PDF logic entirely out of the equation until I was ready for that. When I was ready to save, only then I would update the PDFDocument. This made I/O much easier to debug too because I wasn't updating the PDFDocument during UI callbacks. It was all sequential code. I could debug this part separately, without dealing with the UI at all.


Since you mentioned undo, if you implemented your UI as I describe above, then you can just use default undo behaviour. You'll get everything for free with little or no code.


I had another app that would open Zip files and make them accessible via a local network mount in the Finder. This app was the complete opposite. I had to get real deep and dirty with NSDocument, undo, and other stuff. You don't want to do that. But that doesn't mean I'm any kind of expert on NSDocument. NSDocument is one of those technologies where the more you delve into it, the less confident you get.

Thanks, John. It seems every time I get so far, the finishing tape gets moved further back! 😁


Xcode has come a long way from MPW, but I kind of expected more of these APIs to 'just work', rather than having to rewrite them.