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.
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.