NSDocument doesn't autosave last changes

I had noticed an unsettling behaviour about NSDocument some years ago and created FB7392851, but the feedback didn't go forward, so I just updated it and hopefully here or there someone can explain what's going on.

When running a simple document-based app with a text view, what I type before closing the app may be discarded without notice. To reproduce it, you can use the code below, then:

  1. Type "asdf" in the text view.
  2. Wait until the Xcode console logs "saving". You can trigger it by switching to another app and back again.
  3. Type something else in the text view, such as "asdf" on a new line.
  4. Quit the app.
  5. Relaunch the app. The second line has been discarded.

Am I doing something wrong or is this a bug? Is there a workaround?

class ViewController: NSViewController {

    @IBOutlet var textView: NSTextView!

}

class Document: NSDocument {

    private(set) var text = ""

    override class var autosavesInPlace: Bool {
        return true
    }

    override func makeWindowControllers() {
        let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
        let windowController = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller")) as! NSWindowController
        (windowController.contentViewController as? ViewController)?.textView.string = text
        self.addWindowController(windowController)
    }

    override func data(ofType typeName: String) throws -> Data {
        Swift.print("saving")
        text = (windowControllers.first?.contentViewController as? ViewController)?.textView.string ?? ""
        return Data(text.utf8)
    }

    override func read(from data: Data, ofType typeName: String) throws {
        text = String(decoding: data, as: UTF8.self)
        (windowControllers.first?.contentViewController as? ViewController)?.textView.string = text
    }

}
Answered by DTS Engineer in 853894022

What you described is an as-designed behavior. Basically, the autosave feature automatically saves the document (if there are unsaved changes) every once a while, or when some events happen (such as when the app is activated or deactivated). If you made some changes on the document, and kill the app before autosave is triggered, the changes will be lost.

On macOS, a user can kill an app in different ways – They can run the Quit app menu by pressing CMD+Q, run the Finder > Force Quit... menu, or execute kill -9 from Terminal.app, or even turn off the power.

To avoid losing the unsaved changes, folks typically implement applicationShouldTerminate(_:), which is triggered when the user runs the Quit app menu (CMD+Q). For a NSDocument-based app, however, this is not needed (assuming the document is correctly marked dirty) because AppKit handles that for you.

Force Quit and kill -9 are meant to quit the app immediately, and so there is no good way to run your code when the events happen. I won't worry too much though, because when a user quits an app in those ways, they are clear that they'd risk data loss.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Thanks. I added a link to this post to the feedback.

NSDocument.fileURL is nil for a new document that hasn't been saved – In that case, when you try to terminate your app or close the document window, AppKit shows an alert asking if you'd save the document, and so you should be fine.

Yes, but only assuming that the document has been marked as dirty... which is what we're trying to work around.

I noticed that there is another issue now:

  1. Create a new document.
  2. Type a letter.
  3. Switch to another app and back again.
  4. Type another letter.
  5. Quit and restart the app.
  6. The document is correctly restored, but when selecting File > Close (or hitting Command-W), it is closed without asking whether I want to save or discard it, and so it probably stays in its autosave directory without the user knowing where it is.

This is my current implementation:

class Document: NSDocument {
    
    private var textStorage: NSTextStorage!
    private(set) var savedText: String?
    
    override func data(ofType typeName: String) throws -> Data {
        savedText = textStorage.string
        ...
    }
    
}

func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
    var remaining = 1
    for document in documents as! [Document] {
        if document.textStorage.string != document.savedText, let fileURL = document.fileURL, let fileType = document.fileType {
            remaining += 1
            document.save(to: fileURL, ofType: fileType, for: .saveOperation) { [self] error in
                if let error = error {
                    presentError(error)
                } else {
                    remaining -= 1
                    if remaining == 0 {
                        NSApp.reply(toApplicationShouldTerminate: true)
                    }
                }
            }
        }
    }
    remaining -= 1
    return remaining == 0 ? .terminateNow : .terminateLater
}

Forgot this bit in the Document class which makes sure that documents that are only opened but not changed are not saved on quit again:

override func read(from url: URL, ofType typeName: String) throws {
    ...
    savedText = textStorage.string
}
NSDocument doesn't autosave last changes
 
 
Q