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 myModel = MyModel()
var body: some Scene {
WindowGroup {
myDocumentView()
.environment(myModel)
}
}
}
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 SQLite database.
At first, it's easy to switch to a DocumentGroup:
@main
struct MyApp: App {
@State private var myModel = MyModel()
var body: some Scene {
DocumentGroup(newDocument: {MyModel() } ) { file in
myDocumentView()
}
}
}
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 MyModel: 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 top level document view looks like this:
import SwiftUI
import CoreGraphics
struct myDocumentView: View {
@Environment(MyModel.self) private var myModel
@Environment(\.undoManager) var undoManager
init(document: MyModel) {
self.myModel = document // but of course this is wrong!
}
... etc.
Thanks to https://stackoverflow.com/users/259521/malhal for showing an approach that lets ReferenceFileDocument conform to @Observable.