Search code examples
swiftuinsundomanagerdocument-based

Quickest way to dirty a FileReferenceDocument in SwiftUI for macOS


I have a number of changes to a document-based application which are not undoable (conceptually, think quiz progress) but which should dirty the document so the state is saved when the user quits or closes the document.

Apple's documentation for ReferenceFileDocument states Changes to the document are observed and on change dirty the document state for resaving. but that does not happen for me. (I have tried both changes to a DocumentData class held by my document and to a variable directly within my ReferenceFileDocument subclass, but nothing happens; the document stays clean and does not autosave.)

NSDocument has an updateChangeCount function which is all I want at this point.

All references to dirtying documents I have found involve an undo manager, so in my document class I have implemented

    var isDirty: Bool = false
    
    func dirtyDocument(undoManager: UndoManager?){
        undoManager?.registerUndo(withTarget: self) { isDirty in
undoManager?.removeAllActions(withTarget: self)
    }
        if isDirty == false {
            isDirty = true
        }
    }

and any View that makes a noteworthy change contains

@Environment(\.undoManager) var undoManager

and calls document.dirtyDocument(undoManager: undoManager)

This works in that it dirties the document, but leaves me with an enabled 'Undo' action which does nothing BUT which marks the document as 'not dirty' if the user invokes it, which is why I added undoManager?.removeAllActions(withTarget: self)

I now have the functionality I want, apart from not finding a way to disable the undo menu item for this 'undo action' but it still feels hackish. Is there a better way to emulate updateChangeCount without invoking the UndoManager and and immediately discarding its changes?


Solution

  • I've come back to this issue because while the above works well enough, I wasn't satisfied.

    Turns out that behind the scenes, both FileDocument and ReferenceFileDocument still use NSDocumentController (I have not found this in the documentation, it was just mentioned in passing in a discussion of FileDocument). Here I've used a button to dirty a document that otherwise does nothing.

    struct ContentView: View {
        @Binding var document: DocumentAppFromTemplateDocument
    
        let documentController: NSDocumentController = .shared
        
        
        var body: some View {
            VStack{
                //TextEditor(text: $document.text)
                Text("App content goes here")
                Button("Dirty the Doc"){
                    if let document = documentController.currentDocument {
                        document.updateChangeCount(.changeDone)
                      }
                        
                }
            
            }
        }
    }
    

    so we can proceed as before.

    (MacOS 14.5, Xcode 16b)