import SwiftUI import UniformTypeIdentifiers import Foundation @main struct DocumentBasedMVVMApp: App { @StateObject var viewModel = TheViewModel() var body: some Scene { #if os(iOS) DocumentGroupLaunchScene("Title") { NewDocumentButton("New motor") } #endif DocumentGroup(newDocument: TheDocument()) { file in DocumentView(doc: file.$document, viewModel: viewModel) .onChange(of: file.document) { oldDoc, newDoc in viewModel.updateFromDocument(newDoc) } .onAppear() { viewModel.updateFromDocument(file.document) viewModel.updateArea() } } } } /// The main page for any documents struct DocumentView: View { @Binding var doc: TheDocument @ObservedObject var viewModel: TheViewModel @Environment(\.dismiss) var dismiss @State private var showingEditingView = false var body: some View { Text("Computed area: \(viewModel.computedArea)") HStack { Button("Cancel") { dismiss() } Button("Geometry") { showingEditingView.toggle() } .fullScreenCover(isPresented: $showingEditingView) { EditingView(doc: $doc, viewModel: viewModel) } } .padding() } } /// Example of a view dedicated to modifying the document and doing some computation in the model, via the model viewer. struct EditingView: View { @Binding var doc: TheDocument @ObservedObject var viewModel: TheViewModel @Environment(\.dismiss) var dismiss var body: some View { ScrollView { HStack { Text("Stator OD") TextField("Stator OD", value: $doc.staOD, format: .number) } HStack { Text("Stator ID") TextField("Stator ID", value: $doc.staID, format: .number) } } HStack { Button("Cancel", systemImage: "xmark") { dismiss() } Button("Save", systemImage: "hand.thumbsup") { viewModel.updateArea() dismiss() } } .padding() } } /// Default file extension from the template extension UTType { static var exampleText: UTType { UTType(importedAs: "com.example.plain-text") } } /// This is the structure I have so far /// Equatable because of .onChange(of: file.document) in the main app view struct TheDocument: FileDocument, Equatable { var staOD = 80.0 // Stator Outter Diameter var staID = 50.0 // Stator Inner Diameter init() { } static var readableContentTypes: [UTType] { [.exampleText] } init(configuration: ReadConfiguration) throws { guard let data = configuration.file.regularFileContents, let content = String(data: data, encoding: .utf8) else { throw CocoaError(.fileReadCorruptFile) } let lines = content.split(separator: "\n").map { String($0) } for line in lines { let components = line.split(separator: "=", maxSplits: 1).map { String($0) } if components.count == 2 { let key = components[0] let value = components[1] switch key { case "staOD": self.staOD = Double(value) ?? 80.0 case "staID": self.staID = Double(value) ?? 50.0 default: print("Unknown key in TheDocument initializer: \(key)") continue } } } } func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { let content = """ staOD=\(staOD) staID=\(staID) """ guard let data = content.data(using: .utf8) else { throw CocoaError(.fileWriteInapplicableStringEncoding) } return .init(regularFileWithContents: data) } /// For Equatable static func == (lhs: TheDocument, rhs: TheDocument) -> Bool { return lhs.staOD == rhs.staOD && lhs.staID == rhs.staID } } /// Previously, this ViewModel had `@Published var` instead of `private(set) var` /// I used to edit directly the values of the view model in my TextFields. /// However, I thought the document would now play that role, so the ViewModel is left with button callbacks and interfacing with the model class TheViewModel: ObservableObject { private static func createTheModel() -> (TheModel) { return TheModel() } private(set) var theModel = createTheModel() private(set) var staOD = 80.0 private(set) var staID = 50.0 private(set) var computedArea = 0.0 public func updateFromDocument(_ doc: TheDocument) { self.staOD = doc.staOD self.staID = doc.staID } private func transferToModel() { theModel.staOD = staOD theModel.staID = staID } public func updateArea() { transferToModel() theModel.calcArea() computedArea = theModel.area } } /// Huge structure with a lot of parameters and complex computations struct TheModel { var area = 0.0 var staOD = 0.0 var staID = 0.0 mutating func calcArea() { area = .pi/4 * (pow(staOD,2) - pow(staID,2)) } }