Can a ReferenceFileDocument be @Observable?

Although I can't see anything in Apple's documentation to this effect, I'm coming to believe that ReferenceFileDocument is incompatible with @Observable.

But hopefully I've just missed something!

I have an app in which the data model is @Observable, and views see it through

@Environment(dataModel.self) private var dataModel

Since there are a large number of views, only some of which may need to be redrawn at a given time, Apple's documentation leads me to believe that @Observable may be smarter about only redrawing views that actually need redrawing than @Published and @ObservedObject.

I originally wrote the app without document persistence, and injected the data model into the environment like this:

@main
struct MyApp: App {
    @State private var dataModel = DataModel()

    var body: some Scene {
        WindowGroup {
            myDocumentView()
               .environment(dataModel)
        }
    }
}

I’ve been trying to make the app document based. Although I started using SwiftData, it has trouble with Codable (you need to explicitly code each element), and a long thread in the Developer forum suggests that SwiftData does not support the Undo manager - and in any event, simple JSON serialization is all that this app requires - not a whole embedded SQLLite database.

At first, it seems to be easy to switch to a DocumentGroup:

@main
struct MyApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: {DataModel() } ) { file in
            myDocumentView()
                .environment(file.document)        }
    }
}

Since I've written everything using @Observable, I thought that I'd make my data model conform to ReferenceFileDocument like this:

import SwiftUI
import SwiftData
import UniformTypeIdentifiers

@Observable class DataModel: Identifiable, Codable, @unchecked Sendable, ReferenceFileDocument {

// Mark: ReferenceFileDocument protocol
    
    static var readableContentTypes: [UTType] {
        [.myuttype]
    }
    
    required init(configuration: ReadConfiguration) throws {
        if let data = configuration.file.regularFileContents {
            let decodedModel = try MyModel(json: data)
            if decodedModel != nil {
                self = decodedModel!
            } else {
                print("Unable to decode the document.")
            }
        } else {
            throw CocoaError(.fileReadCorruptFile)
        }
    }
    
    func snapshot(contentType: UTType) throws -> Data {
        try self.json()
    }
    
    func fileWrapper(snapshot: Data,
                     configuration: WriteConfiguration) throws -> FileWrapper {
        FileWrapper(regularFileWithContents: snapshot)
    }


    
    var nodes  = [Node]()  // this is the actual data model
    
    init() {
        newDocument()
    }
  ... etc.  

I've also tried a similar approach in which the ReferenceFileDocument is a separate module that serializes an instance of the data model.

The problem I'm currently experiencing is that I can't figure out how to: a) inject the newly created, or newly deserialized data model into the environment so that views can take advantage of it's @Observable properties, or b) how to cause changes in the @Observable data model to trigger serialization (actually I can observe them triggering serialization, but what's being serialized is an empty instance of the data model).

I make data model changes through a call to the Undo manager:

    // MARK: - Undo

    func undoablyPerform(_ actionName: String, with undoManager: UndoManager? = nil, doit: () -> Void) {
        let oldNodes = self.nodes
        doit()
        undoManager?.registerUndo(withTarget: self) { myself in
            self.undoablyPerform(actionName, with: undoManager) {
                self.nodes = oldNodes
            }
        }
        undoManager?.setActionName(actionName)
    }

The views looks like this:

import SwiftUI
import CoreGraphics

struct myDocumentView: View {
    @Environment(DataModel.self) private var dataModel    
    @Environment(\.undoManager) var undoManager

... etc.

Some things work - if I prepopulate the model, it serializes correctly, and gets written to a file.

Unfortunately, in the view hierarchy, myModel is always empty.

Have I done something wrong? Do I need to abandon @Observable?

I've tried conforming the model to ObservedObject, adding @Published, and injecting it as an @ObservedObject - and viewing as @EnvironmentObject var dataModel: DataModel But it's still not injected correctly into the View hierarchy.

Edit - I may have identified the problem - will update this question when confirmed.

Answered by DTS Engineer in 845329022

Observation seems to work correctly when I use ObservedObject and @Published . BUT, when I switch back to @Observable and @Environment (as it was before making the app document based), the application hangs mysteriously.

ReferenceFileDocument already conforms to ObservableObject. You'll run into unexpected behaviors having the same class be both Observable and ObservableObject.

If I use ReferenceFileDocument , you'll need to go with ObservableObject, that said, you might want to consider storing your document as a value type and use FileDocument instead.

Update:

Observation seems to work correctly when I use ObservedObject and @Published .

BUT, when I switch back to @Observable and @Environment (as it was before making the app document based), the application hangs mysteriously.

I'd sure love to see a working example of a document based app that uses an @Observable data model! Particularly if it used a non-trivial data model, and isolates the document behaviour elegantly.

For now, I'm assuming that it's just not possible, although I'd expect it to be a common use case. BTW, one reason that I use @Observable is that I have hundreds of views, and I'm assuming that observation will be more efficient if I don't explicitly pass a binding to the model in every one of those views.

Accepted Answer

Observation seems to work correctly when I use ObservedObject and @Published . BUT, when I switch back to @Observable and @Environment (as it was before making the app document based), the application hangs mysteriously.

ReferenceFileDocument already conforms to ObservableObject. You'll run into unexpected behaviors having the same class be both Observable and ObservableObject.

If I use ReferenceFileDocument , you'll need to go with ObservableObject, that said, you might want to consider storing your document as a value type and use FileDocument instead.

Thanks for confirming that ReferenceFileDocument and @Observable are incompatible.

Can a ReferenceFileDocument be @Observable?
 
 
Q