7.5 KiB
Graduator
Graduator is an iOS application developed with SwiftUI that helps users manage their academic units
, subjects
, and grades. Users can add and delete subjects
, edit the weight and name of subjects
and units
, and input grades. The app displays weighted averages and explains the conditions for graduating from the Clermont Auvergne Tech Institute's mobile development BSc in 2023.


Features
Beyond those basic features, some details need to be specified here.
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.


Deleting a Subject
In the app, users can delete a subject
by swiping it off the list, right-to-left.


Note that when a subject
is deleted, it is permanently removed from the system. If a user is in the process of editing and deletes a subject
, the deletion occurs immediately upon swiping, not when they save (click 'OK'). If the user chooses to cancel their edits (click 'Annuler'), all other unsaved changes will be discarded, but the deletion of the subject
remains.
Changing a grade
Before a user changes a grade, they first need to activate the lock.open
toggle.

After a grade was changed, in order to save the change and to see it reflected in the unit
's weighted average, users need to use the (lock.open
previously) checkmark
toggle.

Creating a Subject
Finally, users can create a subject
when in edit mode. After clicking on 'Modifier', look for a +
in the top navigation bar.


Architecture
Graduator is based on the MVVM (Model-View-ViewModel) architectural pattern. The below UML class diagram details the structure of the models, viewmodels, and views for UnitsManager
, Unit
, and Subject
.
classDiagram
class Unit {
+name: String
+weight: Int
+isProfessional: Bool
+code: Int
+subjects: [Subject]
+getAverage(): Double?
+data: Data
+update(from: Data): Void
}
class UnitVM {
-original: Unit
+model: Unit.Data
+isEdited: Bool
+SubjectsVM: [SubjectVM]
+onEditing(): Void
+onEdited(isCancelled: Bool): Void
+updateSubject(subjectVM: SubjectVM): Void
+updateAllSubjects(): Void
+deleteSubject(subjectVM: SubjectVM): Void
+addSubject(subject: Subject): Void
+Average: Double?
}
class UnitView {
-unitVM: UnitVM
-unitsManagerVM: UnitsManagerVM
+delete(at: IndexSet): Void
}
class Subject {
+name: String
+weight: Int
+grade: Double?
+gradeIsValid(grade: Double?): Bool
+data: Data
+update(from: Data): Void
}
class SubjectVM {
-original: Subject
+model: Subject.Data
+isEdited: Bool
+onEditing(): Void
+onEdited(isCancelled: Bool): Void
}
class SubjectViewCell {
-subjectVM: SubjectVM
-unitVM: UnitVM
-unitsManagerVM: UnitsManagerVM
-isGradeEditable: Bool
}
class UnitsManager {
+units: [Unit]
+getTotalAverage(): Double?
+getProfessionalAverage(): Double?
+getAverage(units: [Unit]): Double?
+data: Data
+update(from: Data): Void
}
class UnitsManagerVM {
-original: UnitsManager
+model: UnitsManager.Data
+isEdited: Bool
+isAllEditable: Bool
+UnitsVM: [UnitVM]
+updateUnit(unitVM: UnitVM): Void
+TotalAverage: Double?
+ProfessionalAverage: Double?
}
class MainView {
-unitsManagerVM: UnitsManagerVM
}
UnitVM -- Unit : Uses
UnitView -- UnitVM : Observes
SubjectVM -- Subject : Uses
SubjectViewCell -- SubjectVM : Observes
UnitVM "1" o-- "*" SubjectVM : Contains
UnitsManagerVM -- UnitsManager : Uses
UnitsManagerVM "1" o-- "*" UnitVM : Contains
MainView -- UnitsManagerVM : Observes
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.
Here is the diagram with those relationships depicted.
classDiagram
class Unit {
+name: String
+weight: Int
+isProfessional: Bool
+code: Int
+subjects: [Subject]
+getAverage(): Double?
+data: Data
+update(from: Data): Void
}
class UnitVM {
-original: Unit
+model: Unit.Data
+isEdited: Bool
+SubjectsVM: [SubjectVM]
+onEditing(): Void
+onEdited(isCancelled: Bool): Void
+updateSubject(subjectVM: SubjectVM): Void
+updateAllSubjects(): Void
+deleteSubject(subjectVM: SubjectVM): Void
+addSubject(subject: Subject): Void
+Average: Double?
}
class UnitView {
-unitVM: UnitVM
-unitsManagerVM: UnitsManagerVM
+delete(at: IndexSet): Void
}
class Subject {
+name: String
+weight: Int
+grade: Double?
+gradeIsValid(grade: Double?): Bool
+data: Data
+update(from: Data): Void
}
class SubjectVM {
-original: Subject
+model: Subject.Data
+isEdited: Bool
+onEditing(): Void
+onEdited(isCancelled: Bool): Void
}
class SubjectViewCell {
-subjectVM: SubjectVM
-unitVM: UnitVM
-unitsManagerVM: UnitsManagerVM
-isGradeEditable: Bool
}
class UnitsManager {
+units: [Unit]
+getTotalAverage(): Double?
+getProfessionalAverage(): Double?
+getAverage(units: [Unit]): Double?
+data: Data
+update(from: Data): Void
}
class UnitsManagerVM {
-original: UnitsManager
+model: UnitsManager.Data
+isEdited: Bool
+isAllEditable: Bool
+UnitsVM: [UnitVM]
+updateUnit(unitVM: UnitVM): Void
+TotalAverage: Double?
+ProfessionalAverage: Double?
}
class MainView {
-unitsManagerVM: UnitsManagerVM
}
UnitVM -- Unit : Uses
UnitView -- UnitVM : Observes
SubjectVM -- Subject : Uses
SubjectViewCell -- SubjectVM : Observes
UnitVM "1" o-- "*" SubjectVM : Contains
Unit "1" o-- "*" Subject : Contains
UnitsManagerVM -- UnitsManager : Uses
UnitsManagerVM "1" o-- "*" UnitVM : Contains
UnitsManager "1" o-- "*" Unit : Contains
MainView -- UnitsManagerVM : Observes