💾 Fix #3: Implement (local) persistence #8

Merged
alexis.drai merged 7 commits from feature/add-persistence into main 2 years ago

@ -19,6 +19,9 @@
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 */; };
EC8BAD1A2A34BC170062226B /* WeightedGrade.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8BAD192A34BC170062226B /* WeightedGrade.swift */; };
EC8BAD1C2A34BE4C0062226B /* WeightedAverageCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8BAD1B2A34BE4C0062226B /* WeightedAverageCalculator.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 +44,9 @@
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>"; };
EC8BAD192A34BC170062226B /* WeightedGrade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeightedGrade.swift; sourceTree = "<group>"; };
EC8BAD1B2A34BE4C0062226B /* WeightedAverageCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeightedAverageCalculator.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 +114,14 @@
path = Utils; path = Utils;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
EC8BAD142A34650D0062226B /* Data */ = {
isa = PBXGroup;
children = (
EC8BAD152A3465230062226B /* UnitsStore.swift */,
);
path = Data;
sourceTree = "<group>";
};
ECB2FFCC2A23C49500FF9F91 /* Forms */ = { ECB2FFCC2A23C49500FF9F91 /* Forms */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -135,12 +149,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 */,
); );
@ -169,9 +183,12 @@
ECE6E3C02A1F80F6004FE471 /* Model */ = { ECE6E3C02A1F80F6004FE471 /* Model */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
EC242B822A1FAA9B006FE760 /* Stub.swift */,
EC242B6B2A1F81AE006FE760 /* Subject.swift */, EC242B6B2A1F81AE006FE760 /* Subject.swift */,
EC242B692A1F8189006FE760 /* Unit.swift */, EC242B692A1F8189006FE760 /* Unit.swift */,
EC242B6D2A1F81CC006FE760 /* UnitsManager.swift */, EC242B6D2A1F81CC006FE760 /* UnitsManager.swift */,
EC8BAD192A34BC170062226B /* WeightedGrade.swift */,
EC8BAD1B2A34BE4C0062226B /* WeightedAverageCalculator.swift */,
); );
path = Model; path = Model;
sourceTree = "<group>"; sourceTree = "<group>";
@ -251,8 +268,11 @@
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 */,
EC8BAD1C2A34BE4C0062226B /* WeightedAverageCalculator.swift in Sources */,
EC8BAD1A2A34BC170062226B /* WeightedGrade.swift in Sources */,
EC242B6C2A1F81AE006FE760 /* Subject.swift in Sources */, EC242B6C2A1F81AE006FE760 /* Subject.swift in Sources */,
EC242B8A2A1FCECA006FE760 /* AverageBlockView.swift in Sources */, EC242B8A2A1FCECA006FE760 /* AverageBlockView.swift in Sources */,
ECB2FFD02A23C4B700FF9F91 /* SubjectFormVM.swift in Sources */, ECB2FFD02A23C4B700FF9F91 /* SubjectFormVM.swift in Sources */,

@ -0,0 +1,48 @@
//
// UnitsStore.swift
// Graduator
//
// Created by etudiant on 2023-06-10.
//
import SwiftUI
class UnitsStore: ObservableObject {
private static func fileURL() throws -> URL {
try FileManager.default.url(
for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false
).appendingPathComponent("dat.data.tho")
}
func load<T: Codable>(defaultValue: [T]) async throws -> [T] {
let task = Task<[T], Error> {
let fileURL = try Self.fileURL()
let data = try? Data(contentsOf: fileURL)
var elements: [T] = defaultValue
if let validData = data, !validData.isEmpty {
elements = try JSONDecoder().decode([T].self, from: validData)
}
return elements
}
return try await task.value
}
func save<T: Codable>(elements: [T]) async throws {
let task = Task {
let data = try JSONEncoder().encode(elements)
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)
} }
} }

@ -121,7 +121,7 @@ struct Stub {
), ),
Subject( Subject(
id: UUID(), id: UUID(),
name: "Architecture de projetc C# .NET (1)", name: "Architecture de projet C# .NET (1)",
weight: 5, weight: 5,
grade: 14.5/20.0 grade: 14.5/20.0
), ),
@ -148,7 +148,7 @@ struct Stub {
subjects: [ subjects: [
Subject( Subject(
id: UUID(), id: UUID(),
name: "Architecture de projetc C# .NET (2)", name: "Architecture de projet C# .NET (2)",
weight: 4, weight: 4,
grade: 12.17/20.0 grade: 12.17/20.0
), ),

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

@ -7,27 +7,16 @@
import Foundation import Foundation
struct Unit : Identifiable { struct Unit : Identifiable, Codable, WeightedGrade {
let id: UUID let id: UUID
var name: String var name: String
var weight: Int var weight: Int
var grade: Double? { getAverage() }
var isProfessional: Bool var isProfessional: Bool
var code: Int var code: Int
var subjects: [Subject] var subjects: [Subject]
func getAverage() -> Double? { func getAverage() -> Double? {
var totalWeight = 0 return WeightedAverageCalculator.average(elements: subjects)
var weightedSum = 0.0
for subject in subjects {
if let grade = subject.grade {
totalWeight += subject.weight
weightedSum += grade * Double(subject.weight)
}
}
guard totalWeight > 0 else { return nil }
return weightedSum / Double(totalWeight)
} }
} }

@ -10,28 +10,37 @@ import Foundation
struct UnitsManager { struct UnitsManager {
var units: [Unit] var units: [Unit]
private var store = UnitsStore()
func getTotalAverage() -> Double? {
return getAverage(units: units) public init(units: [Unit] = [], store: UnitsStore = UnitsStore()) {
self.units = units
self.store = store
} }
func getProfessionalAverage() -> Double? { mutating func load() async throws {
return getAverage(units: units.filter { $0.isProfessional }) do {
self.units = try await store.load(defaultValue: Stub.units)
} catch {
// DEV: this should be replaced with proper error handling before ever going to prod
print("ERROR: Failed to load...")
}
} }
func getAverage(units: [Unit]) -> Double? { func save() async throws {
var totalWeight = 0 do {
var weightedSum = 0.0 try await store.save(elements: units)
} catch {
for unit in units { // DEV: this should be replaced with proper error handling before ever going to prod
if let grade = unit.getAverage() { print("ERROR: Failed to save...")
totalWeight += unit.weight
weightedSum += grade * Double(unit.weight)
}
} }
}
guard totalWeight > 0 else { return nil }
func getTotalAverage() -> Double? {
return weightedSum / Double(totalWeight) return WeightedAverageCalculator.average(elements: units)
}
func getProfessionalAverage() -> Double? {
let professionalUnits = units.filter { $0.isProfessional }
return WeightedAverageCalculator.average(elements: professionalUnits)
} }
} }

@ -0,0 +1,26 @@
//
// WeightedAverageCalculator.swift
// Graduator
//
// Created by etudiant on 2023-06-10.
//
import Foundation
struct WeightedAverageCalculator {
static func average<T: WeightedGrade>(elements: [T]) -> Double? {
var totalWeight = 0
var weightedSum = 0.0
for element in elements {
if let grade = element.grade {
totalWeight += element.weight
weightedSum += grade * Double(element.weight)
}
}
guard totalWeight > 0 else { return nil }
return weightedSum / Double(totalWeight)
}
}

@ -0,0 +1,13 @@
//
// WeightedGrade.swift
// Graduator
//
// Created by etudiant on 2023-06-10.
//
import Foundation
protocol WeightedGrade {
var weight: Int { get }
var grade: Double? { get }
}

@ -33,9 +33,16 @@ 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 {
// DEV: this should be replaced with proper error handling before ever going to prod
print("ERROR: 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 {
@ -77,16 +76,25 @@ struct UnitView: View {
Image(systemName: "plus") Image(systemName: "plus")
} }
Button(action: { Button(action: {
unitVM.isEdited = false
unitsManagerVM.isAllEditable.toggle() unitsManagerVM.isAllEditable.toggle()
unitVM.onEdited(isCancelled: true) unitVM.onEdited(isCancelled: true)
unitVM.SubjectsVM.forEach { $0.onEdited(isCancelled: true) }
}) { }) {
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 {
// DEV: this should be replaced with proper error handling before ever going to prod
print("ERROR: Failed to update unit: \(error)")
}
}
}) { }) {
Text("OK") Text("OK")
} }
@ -107,9 +115,16 @@ 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 {
// DEV: this should be replaced with proper error handling before ever going to prod
print("ERROR: Failed to create unit: \(error)")
}
}
} else { } else {
showAlert = true showAlert = true
} }
@ -124,6 +139,7 @@ struct UnitView: View {
// If user navigates back while editing but before clicking 'OK', the changes are cancelled // If user navigates back while editing but before clicking 'OK', the changes are cancelled
.onDisappear(perform: { .onDisappear(perform: {
if unitsManagerVM.isAllEditable { if unitsManagerVM.isAllEditable {
unitVM.isEdited = false
unitVM.onEdited(isCancelled: true) unitVM.onEdited(isCancelled: true)
unitsManagerVM.isAllEditable = false unitsManagerVM.isAllEditable = false
} }

@ -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() {

@ -44,9 +44,8 @@ class UnitsManagerVM : ObservableObject {
private var original: UnitsManager private var original: UnitsManager
@Published var model: UnitsManager.Data @Published var model: UnitsManager.Data
@Published var isEdited: Bool = false
@Published var isAllEditable: Bool = false @Published var isAllEditable: Bool = false
private var unitsVM: [UnitVM] private var unitsVM: [UnitVM]
public var UnitsVM: [UnitVM] { unitsVM } public var UnitsVM: [UnitVM] { unitsVM }
@ -54,18 +53,39 @@ 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 {
do {
try await original.load()
DispatchQueue.main.async {
self.model = self.original.data
self.unitsVM = self.original.units.map { UnitVM(unit: $0) }
}
} catch {
// DEV: this should be replaced with proper error handling before ever going to prod
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 do {
try await original.save()
DispatchQueue.main.async {
self.model = self.original.data
}
} catch {
// DEV: this should be replaced with proper error handling before ever going to prod
print("ERROR: Failed to save VM...")
}
} }
var TotalAverage: Double? { var TotalAverage: Double? {

@ -9,6 +9,14 @@ Graduator is an iOS application developed with SwiftUI that helps users manage t
Beyond those basic features, some details need to be specified here. Beyond those basic features, some details need to be specified here.
### Persistence
The data is set to be persisted, unless you're using XCode's previewer canvas.
The local persistence solution has been tested manually on the iOS Simulator.
Upon first launching the app, it is set up to load a stub.
### Weighted average ### Weighted average
A weighted average means that a `subject` or `unit`'s weight plays a part in calculating the average. Users can observe that increasing the weight of a `subject`, for instance, will make the average of the parent `unit` tend more towards that `subject`'s grade. A weighted average means that a `subject` or `unit`'s weight plays a part in calculating the average. Users can observe that increasing the weight of a `subject`, for instance, will make the average of the parent `unit` tend more towards that `subject`'s grade.
@ -39,6 +47,7 @@ After a grade was changed, in order to save the change and to see it reflected i
Finally, users can create a `subject` when in edit mode. After clicking on *'Modifier'*, look for a `+` in the top navigation bar. Finally, users can create a `subject` when in edit mode. After clicking on *'Modifier'*, look for a `+` in the top navigation bar.
<img src="./docs/delete_2.png" height="700" style="margin:20px" alt="subject deleted">
<img src="./docs/create_1.png" height="700" style="margin:20px" alt="creating a subject"> <img src="./docs/create_1.png" height="700" style="margin:20px" alt="creating a subject">
<img src="./docs/create_2.png" height="700" style="margin:20px" alt="subject created"> <img src="./docs/create_2.png" height="700" style="margin:20px" alt="subject created">
@ -130,9 +139,9 @@ classDiagram
It might be useful to note that, just like `UnitVM`s aggregate `SubjectVM`s, `Unit`s aggregate It might be useful to note that, just like `UnitVM`s aggregate `SubjectVM`s, `Unit`s aggregate
`Subject`s, but these relationship between `Model` entities were removed from the diagram above for clarity. `Subject`s, but these relationship between `Model` entities were removed from the diagram above for clarity.
The same is true with the View-related classes. The same is true with the `View`-related classes.
Here is the diagram with those relationships depicted. Here is the diagram with those relationships depicted, and the local persistence solution added.
@ -146,6 +155,7 @@ classDiagram
class UnitsManagerVM { class UnitsManagerVM {
-original: UnitsManager -original: UnitsManager
+load()
+model: UnitsManager.Data +model: UnitsManager.Data
+isEdited: Bool +isEdited: Bool
+isAllEditable: Bool +isAllEditable: Bool
@ -175,6 +185,9 @@ classDiagram
class UnitsManager { class UnitsManager {
-store: UnitsStore
+save()
+load()
+getTotalAverage(): Double? +getTotalAverage(): Double?
+getProfessionalAverage(): Double? +getProfessionalAverage(): Double?
+getAverage(units: Unit[]): Double? +getAverage(units: Unit[]): Double?
@ -198,6 +211,11 @@ classDiagram
+data: Data +data: Data
+update(from: Data) +update(from: Data)
} }
class UnitsStore {
+load<T: Codable>(defaultValue: T[])
+save<T: Codable>(elements: T[])
}
MainView --> "*" UnitView MainView --> "*" UnitView
MainView --> UnitsManagerVM MainView --> UnitsManagerVM
@ -215,5 +233,8 @@ classDiagram
SubjectVM --> Subject SubjectVM --> Subject
UnitsManager --> "*" Unit UnitsManager --> "*" Unit
UnitsManager --> UnitsStore
UnitsManager --> Stub
Stub --> "*" Unit
Unit --> "*" Subject Unit --> "*" Subject
``` ```

Loading…
Cancel
Save