💾 Implement (local) persistence

pull/8/head
Alexis Drai 2 years ago
parent efa89c4d3d
commit 644616784a

@ -19,6 +19,7 @@
EC242B882A1FC605006FE760 /* NoGradesInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC242B872A1FC605006FE760 /* NoGradesInfo.swift */; }; EC242B882A1FC605006FE760 /* NoGradesInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC242B872A1FC605006FE760 /* NoGradesInfo.swift */; };
EC242B8A2A1FCECA006FE760 /* AverageBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC242B892A1FCECA006FE760 /* AverageBlockView.swift */; }; EC242B8A2A1FCECA006FE760 /* AverageBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC242B892A1FCECA006FE760 /* AverageBlockView.swift */; };
EC5FE5A52A20882F0028AA5F /* Formatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5FE5A42A20882F0028AA5F /* Formatters.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 */; }; ECB2FFCE2A23C4A700FF9F91 /* SubjectFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB2FFCD2A23C4A700FF9F91 /* SubjectFormView.swift */; };
ECB2FFD02A23C4B700FF9F91 /* SubjectFormVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB2FFCF2A23C4B700FF9F91 /* SubjectFormVM.swift */; }; ECB2FFD02A23C4B700FF9F91 /* SubjectFormVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB2FFCF2A23C4B700FF9F91 /* SubjectFormVM.swift */; };
ECC581D22A1D085B006C55EF /* GraduatorApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC581D12A1D085B006C55EF /* GraduatorApp.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 = "<group>"; }; EC242B872A1FC605006FE760 /* NoGradesInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoGradesInfo.swift; sourceTree = "<group>"; };
EC242B892A1FCECA006FE760 /* AverageBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AverageBlockView.swift; sourceTree = "<group>"; }; EC242B892A1FCECA006FE760 /* AverageBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AverageBlockView.swift; sourceTree = "<group>"; };
EC5FE5A42A20882F0028AA5F /* Formatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Formatters.swift; sourceTree = "<group>"; }; EC5FE5A42A20882F0028AA5F /* Formatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Formatters.swift; sourceTree = "<group>"; };
EC8BAD152A3465230062226B /* UnitsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsStore.swift; sourceTree = "<group>"; };
ECB2FFCD2A23C4A700FF9F91 /* SubjectFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubjectFormView.swift; sourceTree = "<group>"; }; ECB2FFCD2A23C4A700FF9F91 /* SubjectFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubjectFormView.swift; sourceTree = "<group>"; };
ECB2FFCF2A23C4B700FF9F91 /* SubjectFormVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubjectFormVM.swift; sourceTree = "<group>"; }; ECB2FFCF2A23C4B700FF9F91 /* SubjectFormVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubjectFormVM.swift; sourceTree = "<group>"; };
ECC581CE2A1D085B006C55EF /* Graduator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Graduator.app; sourceTree = BUILT_PRODUCTS_DIR; }; ECC581CE2A1D085B006C55EF /* Graduator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Graduator.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -108,6 +110,15 @@
path = Utils; path = Utils;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
EC8BAD142A34650D0062226B /* Data */ = {
isa = PBXGroup;
children = (
EC242B822A1FAA9B006FE760 /* Stub.swift */,
EC8BAD152A3465230062226B /* UnitsStore.swift */,
);
path = Data;
sourceTree = "<group>";
};
ECB2FFCC2A23C49500FF9F91 /* Forms */ = { ECB2FFCC2A23C49500FF9F91 /* Forms */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -135,12 +146,12 @@
ECC581D02A1D085B006C55EF /* Graduator */ = { ECC581D02A1D085B006C55EF /* Graduator */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
EC8BAD142A34650D0062226B /* Data */,
ECE6E3C02A1F80F6004FE471 /* Model */, ECE6E3C02A1F80F6004FE471 /* Model */,
ECC581DF2A1D08C3006C55EF /* ViewModel */, ECC581DF2A1D08C3006C55EF /* ViewModel */,
EC242B6F2A1F8260006FE760 /* View */, EC242B6F2A1F8260006FE760 /* View */,
ECC581D12A1D085B006C55EF /* GraduatorApp.swift */, ECC581D12A1D085B006C55EF /* GraduatorApp.swift */,
EC242B702A1F8283006FE760 /* MainView.swift */, EC242B702A1F8283006FE760 /* MainView.swift */,
EC242B822A1FAA9B006FE760 /* Stub.swift */,
ECC581D52A1D085C006C55EF /* Assets.xcassets */, ECC581D52A1D085C006C55EF /* Assets.xcassets */,
ECC581D72A1D085C006C55EF /* Preview Content */, ECC581D72A1D085C006C55EF /* Preview Content */,
); );
@ -251,6 +262,7 @@
EC242B6E2A1F81CC006FE760 /* UnitsManager.swift in Sources */, EC242B6E2A1F81CC006FE760 /* UnitsManager.swift in Sources */,
ECC581E52A1D0C44006C55EF /* UnitVM.swift in Sources */, ECC581E52A1D0C44006C55EF /* UnitVM.swift in Sources */,
EC5FE5A52A20882F0028AA5F /* Formatters.swift in Sources */, EC5FE5A52A20882F0028AA5F /* Formatters.swift in Sources */,
EC8BAD162A3465230062226B /* UnitsStore.swift in Sources */,
EC242B7F2A1F83BF006FE760 /* UnitsManagerVM.swift in Sources */, EC242B7F2A1F83BF006FE760 /* UnitsManagerVM.swift in Sources */,
EC242B7B2A1F838C006FE760 /* UnitViewCell.swift in Sources */, EC242B7B2A1F838C006FE760 /* UnitViewCell.swift in Sources */,
EC242B6C2A1F81AE006FE760 /* Subject.swift in Sources */, EC242B6C2A1F81AE006FE760 /* Subject.swift in Sources */,

@ -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
}
}

@ -14,6 +14,13 @@ struct GraduatorApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
MainView(unitsManagerVM: unitsManagerVM) MainView(unitsManagerVM: unitsManagerVM)
.task {
do {
try await unitsManagerVM.load()
} catch {
fatalError(error.localizedDescription)
}
}
.environmentObject(unitsManagerVM) .environmentObject(unitsManagerVM)
} }
} }

@ -7,7 +7,7 @@
import Foundation import Foundation
struct Subject : Identifiable { struct Subject : Identifiable, Codable {
let id: UUID let id: UUID
var name: String var name: String
var weight: Int var weight: Int

@ -7,7 +7,7 @@
import Foundation import Foundation
struct Unit : Identifiable { struct Unit : Identifiable, Codable {
let id: UUID let id: UUID
var name: String var name: String
var weight: Int var weight: Int
@ -15,6 +15,7 @@ struct Unit : Identifiable {
var code: Int var code: Int
var subjects: [Subject] var subjects: [Subject]
// FIXME DRY
func getAverage() -> Double? { func getAverage() -> Double? {
var totalWeight = 0 var totalWeight = 0
var weightedSum = 0.0 var weightedSum = 0.0

@ -10,6 +10,32 @@ import Foundation
struct UnitsManager { struct UnitsManager {
var units: [Unit] 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? { func getTotalAverage() -> Double? {
return getAverage(units: units) return getAverage(units: units)
@ -19,6 +45,7 @@ struct UnitsManager {
return getAverage(units: units.filter { $0.isProfessional }) return getAverage(units: units.filter { $0.isProfessional })
} }
// FIXME DRY
func getAverage(units: [Unit]) -> Double? { func getAverage(units: [Unit]) -> Double? {
var totalWeight = 0 var totalWeight = 0
var weightedSum = 0.0 var weightedSum = 0.0

@ -17,6 +17,9 @@ struct SubjectViewCell: View {
var body: some View { var body: some View {
HStack { HStack {
VStack { 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 { HStack {
TextField("", text: $subjectVM.model.name) TextField("", text: $subjectVM.model.name)
.disabled(!unitsManagerVM.isAllEditable) .disabled(!unitsManagerVM.isAllEditable)
@ -33,9 +36,15 @@ struct SubjectViewCell: View {
.frame(width: 40) .frame(width: 40)
.onChange(of: isGradeEditable) { value in .onChange(of: isGradeEditable) { value in
if !value { if !value {
subjectVM.onEdited() Task {
unitVM.updateSubject(subjectVM) do {
unitsManagerVM.updateUnit(unitVM) subjectVM.onEdited()
unitVM.updateSubject(subjectVM)
try await unitsManagerVM.updateUnit(unitVM)
} catch {
print("Failed to update grade: \(error)")
}
}
} }
} }
Image(systemName: isGradeEditable ? "checkmark" : "lock.open") Image(systemName: isGradeEditable ? "checkmark" : "lock.open")

@ -19,7 +19,6 @@ struct UnitView: View {
guard let index = offsets.first else { return } guard let index = offsets.first else { return }
let subjectVMToDelete = unitVM.SubjectsVM[index] let subjectVMToDelete = unitVM.SubjectsVM[index]
unitVM.deleteSubject(subjectVMToDelete) unitVM.deleteSubject(subjectVMToDelete)
unitsManagerVM.updateUnit(unitVM)
} }
var body: some View { var body: some View {
@ -83,10 +82,16 @@ struct UnitView: View {
Text("Annuler") Text("Annuler")
} }
Button(action: { Button(action: {
unitsManagerVM.isAllEditable.toggle() Task {
unitVM.onEdited() do {
unitVM.updateAllSubjects() unitsManagerVM.isAllEditable.toggle()
unitsManagerVM.updateUnit(unitVM) unitVM.onEdited()
unitVM.updateAllSubjects()
try await unitsManagerVM.updateUnit(unitVM)
} catch {
print("Failed to update unit: \(error)")
}
}
}) { }) {
Text("OK") Text("OK")
} }
@ -107,9 +112,15 @@ struct UnitView: View {
leading: Button("Annuler") { showingForm = false }, leading: Button("Annuler") { showingForm = false },
trailing: Button("Enregistrer") { trailing: Button("Enregistrer") {
if let newSubject = formVM.createSubject() { if let newSubject = formVM.createSubject() {
unitVM.addSubject(newSubject) Task {
unitsManagerVM.updateUnit(unitVM) do {
showingForm = false unitVM.addSubject(newSubject)
try await unitsManagerVM.updateUnit(unitVM)
showingForm = false
} catch {
print("Failed to create unit: \(error)")
}
}
} else { } else {
showAlert = true showAlert = true
} }

@ -60,7 +60,7 @@ class UnitVM : ObservableObject, Identifiable {
init(unit: Unit) { init(unit: Unit) {
original = unit original = unit
model = original.data model = original.data
subjectsVM = unit.subjects.map { SubjectVM(subject: $0) } subjectsVM = original.subjects.map { SubjectVM(subject: $0) }
} }
convenience init() { convenience init() {

@ -54,18 +54,41 @@ class UnitsManagerVM : ObservableObject {
init(unitsManager: UnitsManager) { init(unitsManager: UnitsManager) {
original = unitsManager original = unitsManager
model = original.data model = original.data
unitsVM = unitsManager.units.map { UnitVM(unit: $0) } unitsVM = original.units.map { UnitVM(unit: $0) }
} }
convenience init() { convenience init() {
self.init(unitsManager: UnitsManager(units: [])) 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 } guard let index = unitsVM.firstIndex(where: { $0.id == unitVM.id }) else { return }
let updatedUnit = unitsVM[index].model let updatedUnit = unitsVM[index].model
original.units[index].update(from: updatedUnit) 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? { var TotalAverage: Double? {

Loading…
Cancel
Save