diff --git a/Graduator/Graduator.xcodeproj/project.pbxproj b/Graduator/Graduator.xcodeproj/project.pbxproj index 3a41674..8c3656d 100644 --- a/Graduator/Graduator.xcodeproj/project.pbxproj +++ b/Graduator/Graduator.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ EC242B882A1FC605006FE760 /* NoGradesInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC242B872A1FC605006FE760 /* NoGradesInfo.swift */; }; EC242B8A2A1FCECA006FE760 /* AverageBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC242B892A1FCECA006FE760 /* AverageBlockView.swift */; }; EC5FE5A52A20882F0028AA5F /* Formatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5FE5A42A20882F0028AA5F /* Formatters.swift */; }; + EC8BAD162A3465230062226B /* UnitsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8BAD152A3465230062226B /* UnitsStore.swift */; }; ECB2FFCE2A23C4A700FF9F91 /* SubjectFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB2FFCD2A23C4A700FF9F91 /* SubjectFormView.swift */; }; ECB2FFD02A23C4B700FF9F91 /* SubjectFormVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB2FFCF2A23C4B700FF9F91 /* SubjectFormVM.swift */; }; ECC581D22A1D085B006C55EF /* GraduatorApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC581D12A1D085B006C55EF /* GraduatorApp.swift */; }; @@ -41,6 +42,7 @@ EC242B872A1FC605006FE760 /* NoGradesInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoGradesInfo.swift; sourceTree = ""; }; EC242B892A1FCECA006FE760 /* AverageBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AverageBlockView.swift; sourceTree = ""; }; EC5FE5A42A20882F0028AA5F /* Formatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Formatters.swift; sourceTree = ""; }; + EC8BAD152A3465230062226B /* UnitsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsStore.swift; sourceTree = ""; }; ECB2FFCD2A23C4A700FF9F91 /* SubjectFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubjectFormView.swift; sourceTree = ""; }; ECB2FFCF2A23C4B700FF9F91 /* SubjectFormVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubjectFormVM.swift; sourceTree = ""; }; ECC581CE2A1D085B006C55EF /* Graduator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Graduator.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -108,6 +110,15 @@ path = Utils; sourceTree = ""; }; + EC8BAD142A34650D0062226B /* Data */ = { + isa = PBXGroup; + children = ( + EC242B822A1FAA9B006FE760 /* Stub.swift */, + EC8BAD152A3465230062226B /* UnitsStore.swift */, + ); + path = Data; + sourceTree = ""; + }; ECB2FFCC2A23C49500FF9F91 /* Forms */ = { isa = PBXGroup; children = ( @@ -135,12 +146,12 @@ ECC581D02A1D085B006C55EF /* Graduator */ = { isa = PBXGroup; children = ( + EC8BAD142A34650D0062226B /* Data */, ECE6E3C02A1F80F6004FE471 /* Model */, ECC581DF2A1D08C3006C55EF /* ViewModel */, EC242B6F2A1F8260006FE760 /* View */, ECC581D12A1D085B006C55EF /* GraduatorApp.swift */, EC242B702A1F8283006FE760 /* MainView.swift */, - EC242B822A1FAA9B006FE760 /* Stub.swift */, ECC581D52A1D085C006C55EF /* Assets.xcassets */, ECC581D72A1D085C006C55EF /* Preview Content */, ); @@ -251,6 +262,7 @@ EC242B6E2A1F81CC006FE760 /* UnitsManager.swift in Sources */, ECC581E52A1D0C44006C55EF /* UnitVM.swift in Sources */, EC5FE5A52A20882F0028AA5F /* Formatters.swift in Sources */, + EC8BAD162A3465230062226B /* UnitsStore.swift in Sources */, EC242B7F2A1F83BF006FE760 /* UnitsManagerVM.swift in Sources */, EC242B7B2A1F838C006FE760 /* UnitViewCell.swift in Sources */, EC242B6C2A1F81AE006FE760 /* Subject.swift in Sources */, diff --git a/Graduator/Graduator/Stub.swift b/Graduator/Graduator/Data/Stub.swift similarity index 100% rename from Graduator/Graduator/Stub.swift rename to Graduator/Graduator/Data/Stub.swift diff --git a/Graduator/Graduator/Data/UnitsStore.swift b/Graduator/Graduator/Data/UnitsStore.swift new file mode 100644 index 0000000..947cb33 --- /dev/null +++ b/Graduator/Graduator/Data/UnitsStore.swift @@ -0,0 +1,54 @@ +// +// UnitsStore.swift +// Graduator +// +// Created by etudiant on 2023-06-10. +// + +import SwiftUI + + +class UnitsStore: ObservableObject { + + var units: [Unit] = [] + + private static func fileURL() throws -> URL { + try FileManager.default.url( + for: .documentDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: false + ).appendingPathComponent("units.data") + } + + func load() async throws -> [Unit] { + + let task = Task<[Unit], Error> { + let fileURL = try Self.fileURL() + let data = try? Data(contentsOf: fileURL) + var units: [Unit] = [] + if (data == nil || data!.isEmpty) { + units = Stub.units + } + else if (data != nil) { + units = try JSONDecoder().decode([Unit].self, from: data!) + } + return units + } + + self.units = try await task.value + return self.units + } + + func save(units: [Unit]) async throws { + + let task = Task { + let data = try JSONEncoder().encode(units) + let outfile = try Self.fileURL() + try data.write(to: outfile) + } + + _ = try await task.value + } + +} diff --git a/Graduator/Graduator/GraduatorApp.swift b/Graduator/Graduator/GraduatorApp.swift index b35b8dd..54ba97c 100644 --- a/Graduator/Graduator/GraduatorApp.swift +++ b/Graduator/Graduator/GraduatorApp.swift @@ -14,6 +14,13 @@ struct GraduatorApp: App { var body: some Scene { WindowGroup { MainView(unitsManagerVM: unitsManagerVM) + .task { + do { + try await unitsManagerVM.load() + } catch { + fatalError(error.localizedDescription) + } + } .environmentObject(unitsManagerVM) } } diff --git a/Graduator/Graduator/Model/Subject.swift b/Graduator/Graduator/Model/Subject.swift index 4530ca7..e80cdae 100644 --- a/Graduator/Graduator/Model/Subject.swift +++ b/Graduator/Graduator/Model/Subject.swift @@ -7,7 +7,7 @@ import Foundation -struct Subject : Identifiable { +struct Subject : Identifiable, Codable { let id: UUID var name: String var weight: Int diff --git a/Graduator/Graduator/Model/Unit.swift b/Graduator/Graduator/Model/Unit.swift index 2695214..1a1b3d3 100644 --- a/Graduator/Graduator/Model/Unit.swift +++ b/Graduator/Graduator/Model/Unit.swift @@ -7,7 +7,7 @@ import Foundation -struct Unit : Identifiable { +struct Unit : Identifiable, Codable { let id: UUID var name: String var weight: Int @@ -15,6 +15,7 @@ struct Unit : Identifiable { var code: Int var subjects: [Subject] + // FIXME DRY func getAverage() -> Double? { var totalWeight = 0 var weightedSum = 0.0 diff --git a/Graduator/Graduator/Model/UnitsManager.swift b/Graduator/Graduator/Model/UnitsManager.swift index 9e9766e..f075abb 100644 --- a/Graduator/Graduator/Model/UnitsManager.swift +++ b/Graduator/Graduator/Model/UnitsManager.swift @@ -10,6 +10,32 @@ import Foundation struct UnitsManager { var units: [Unit] + private var store = UnitsStore() + + public init(units: [Unit] = [], store: UnitsStore = UnitsStore()) { + self.units = units + self.store = store + } + + mutating func load() async throws { + print("Loading...") + do { + self.units = try await store.load() + print("Units loaded") + } catch { + print("ERROR: Failed to load...") + } + } + + func save() async throws { + print("Saving...") + do { + try await store.save(units: units) + print("Units saved") + } catch { + print("ERROR: Failed to save...") + } + } func getTotalAverage() -> Double? { return getAverage(units: units) @@ -19,6 +45,7 @@ struct UnitsManager { return getAverage(units: units.filter { $0.isProfessional }) } + // FIXME DRY func getAverage(units: [Unit]) -> Double? { var totalWeight = 0 var weightedSum = 0.0 diff --git a/Graduator/Graduator/View/Cells/SubjectViewCell.swift b/Graduator/Graduator/View/Cells/SubjectViewCell.swift index 99cfb74..dc6f7f9 100644 --- a/Graduator/Graduator/View/Cells/SubjectViewCell.swift +++ b/Graduator/Graduator/View/Cells/SubjectViewCell.swift @@ -17,6 +17,9 @@ struct SubjectViewCell: View { var body: some View { HStack { VStack { + // FIXME (later, maybe) when getting updated from the UnitView, these two changes are persisted right away instead of waiting for user + // to confirm changes. Or is it rather that we were updating these fields, and then changing the grade, thus calling + // unitsManagerVM.updateUnit(unitVM) ? If so, how to even fix that? HStack { TextField("", text: $subjectVM.model.name) .disabled(!unitsManagerVM.isAllEditable) @@ -33,9 +36,15 @@ struct SubjectViewCell: View { .frame(width: 40) .onChange(of: isGradeEditable) { value in if !value { - subjectVM.onEdited() - unitVM.updateSubject(subjectVM) - unitsManagerVM.updateUnit(unitVM) + Task { + do { + subjectVM.onEdited() + unitVM.updateSubject(subjectVM) + try await unitsManagerVM.updateUnit(unitVM) + } catch { + print("Failed to update grade: \(error)") + } + } } } Image(systemName: isGradeEditable ? "checkmark" : "lock.open") diff --git a/Graduator/Graduator/View/Views/UnitView.swift b/Graduator/Graduator/View/Views/UnitView.swift index 1605b8a..01a13d0 100644 --- a/Graduator/Graduator/View/Views/UnitView.swift +++ b/Graduator/Graduator/View/Views/UnitView.swift @@ -19,7 +19,6 @@ struct UnitView: View { guard let index = offsets.first else { return } let subjectVMToDelete = unitVM.SubjectsVM[index] unitVM.deleteSubject(subjectVMToDelete) - unitsManagerVM.updateUnit(unitVM) } var body: some View { @@ -83,10 +82,16 @@ struct UnitView: View { Text("Annuler") } Button(action: { - unitsManagerVM.isAllEditable.toggle() - unitVM.onEdited() - unitVM.updateAllSubjects() - unitsManagerVM.updateUnit(unitVM) + Task { + do { + unitsManagerVM.isAllEditable.toggle() + unitVM.onEdited() + unitVM.updateAllSubjects() + try await unitsManagerVM.updateUnit(unitVM) + } catch { + print("Failed to update unit: \(error)") + } + } }) { Text("OK") } @@ -107,9 +112,15 @@ struct UnitView: View { leading: Button("Annuler") { showingForm = false }, trailing: Button("Enregistrer") { if let newSubject = formVM.createSubject() { - unitVM.addSubject(newSubject) - unitsManagerVM.updateUnit(unitVM) - showingForm = false + Task { + do { + unitVM.addSubject(newSubject) + try await unitsManagerVM.updateUnit(unitVM) + showingForm = false + } catch { + print("Failed to create unit: \(error)") + } + } } else { showAlert = true } diff --git a/Graduator/Graduator/ViewModel/UnitVM.swift b/Graduator/Graduator/ViewModel/UnitVM.swift index 703b2fc..3266ba1 100644 --- a/Graduator/Graduator/ViewModel/UnitVM.swift +++ b/Graduator/Graduator/ViewModel/UnitVM.swift @@ -60,7 +60,7 @@ class UnitVM : ObservableObject, Identifiable { init(unit: Unit) { original = unit model = original.data - subjectsVM = unit.subjects.map { SubjectVM(subject: $0) } + subjectsVM = original.subjects.map { SubjectVM(subject: $0) } } convenience init() { diff --git a/Graduator/Graduator/ViewModel/UnitsManagerVM.swift b/Graduator/Graduator/ViewModel/UnitsManagerVM.swift index dc496ce..27e3ab3 100644 --- a/Graduator/Graduator/ViewModel/UnitsManagerVM.swift +++ b/Graduator/Graduator/ViewModel/UnitsManagerVM.swift @@ -46,7 +46,7 @@ class UnitsManagerVM : ObservableObject { @Published var model: UnitsManager.Data @Published var isEdited: Bool = false @Published var isAllEditable: Bool = false - + private var unitsVM: [UnitVM] public var UnitsVM: [UnitVM] { unitsVM } @@ -54,18 +54,41 @@ class UnitsManagerVM : ObservableObject { init(unitsManager: UnitsManager) { original = unitsManager model = original.data - unitsVM = unitsManager.units.map { UnitVM(unit: $0) } + unitsVM = original.units.map { UnitVM(unit: $0) } } convenience init() { self.init(unitsManager: UnitsManager(units: [])) } - func updateUnit(_ unitVM: UnitVM) { + func load() async throws { + print("Loading VM...") + do { + try await original.load() + DispatchQueue.main.async { + self.model = self.original.data + self.unitsVM = self.original.units.map { UnitVM(unit: $0) } + print("VM loaded") + } + } catch { + print("ERROR: Failed to load VM...") + } + } + + func updateUnit(_ unitVM: UnitVM) async throws { guard let index = unitsVM.firstIndex(where: { $0.id == unitVM.id }) else { return } let updatedUnit = unitsVM[index].model original.units[index].update(from: updatedUnit) - model = original.data + print("Saving VM...") + do { + try await original.save() + print("VM saved") + DispatchQueue.main.async { + self.model = self.original.data + } + } catch { + print("ERROR: Failed to save VM...") + } } var TotalAverage: Double? {