Skip to content

draialexis/graduator

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

view from the home page

view from a unit page

Features

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

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.

before changing a subject's weight

after changing a subject's weight

Deleting a subject

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

deleting a subject

subject deleted

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.

changing a grade

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.

grade changed

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.

subject deleted

creating a subject

subject created

Known limitations and issues

Unit and subject weight changes cancel with a delay

ℹ️ There was an issue with subjects getting updated even after a user presses "Annuler". That issue was solved in #6

See #7

When canceling an update to a weight, the value does not instantaneously revert to the old value, like we would expect.

If a user presses "Modifier" again, and then presses "Annuler" again, the old value finally comes back.

We haven't found the source of the issue. The logic is the same as with names, except that simple formatters are involved. Canceling changes made to names is a feature that works as expected.

Since the data appears to be eventually consistent, this issue was left unsolved.

The UI is unintuitive when updating

See #4

When updating names and weights for units and subjects, many users would expect to see a sheet appear.

While we did implement that for creating a new subject, we did not do it for updates. Instead, the text fields used to display the data just become editable when the "Modifier" button is pressed.

Change notifications are not implemented per se

See #5 and this course

Instead, "detail" views observe VMs that are higher up in the hierarchy.

import SwiftUI

struct SubjectViewCell: View {
    @ObservedObject var subjectVM: SubjectVM
    @ObservedObject var unitVM: UnitVM
    @ObservedObject var unitsManagerVM: UnitsManagerVM
        
    @State private var isGradeEditable = false

    var body: some View {
        //...

That way, they can propagate changes by themselves.

Toggle("", isOn: $isGradeEditable)
    //...
    .onChange(of: isGradeEditable) { 
        //...
        subjectVM.onEdited()
        unitVM.updateSubject(subjectVM)
        try await unitsManagerVM.updateUnit(unitVM)
    } //...

It's clunky, it's fragile -- but it's working and so, for lack of time, it's staying for the foreseeable future.

Architecture

Focusing on VMs

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. Notice how, as discussed in this subpart, to circumvent this issue, we insert an entire hierarchy of VMs in certain views, so that they can update all those VMs when a detail gets edited. It's dirty, and it's staying that way for the foreseeable future.

classDiagram

    class MainView
    class UnitView
    class SubjectViewCell

    class UnitsManagerVM {
        -original: UnitsManager
        +model: UnitsManager.Data
        +isEdited: Bool
        +isAllEditable: Bool
        +updateUnit(unitVM: UnitVM): Void
        +TotalAverage: Double?
        +ProfessionalAverage: Double?
    }
    class UnitVM {
        -original: Unit
        +model: Unit.Data
        +isEdited: Bool
        +onEditing()
        +onEdited(isCancelled: Bool)
        +updateSubject(subjectVM: SubjectVM)
        +updateAllSubjects()
        +deleteSubject(subjectVM: SubjectVM)
        +addSubject(subject: Subject)
        +Average: Double?
    }
    class SubjectVM {
        -original: Subject
        +model: Subject.Data
        +isEdited: Bool
        +onEditing(): Void
        +onEdited(isCancelled: Bool): Void
    }
    
    class UnitsManager {
        +getTotalAverage(): Double?
        +getProfessionalAverage(): Double?
        +getAverage(units: Unit[]): Double?
        +data: Data
        +update(from: Data): Void
    }
    class Unit {
        +name: String
        +weight: Int
        +isProfessional: Bool
        +code: Int
        +subjects: Subject[]
        +getAverage(): Double?
        +data: Data
        +update(from: Data): Void
    }
    class Subject {
        +name: String
        +weight: Int
        +grade: Double?
        +gradeIsValid(grade: Double?): Bool
        +data: Data
        +update(from: Data): Void
    }

    MainView --> UnitsManagerVM
    UnitView --> UnitVM
    UnitView --> UnitsManagerVM
    SubjectViewCell --> SubjectVM
    SubjectViewCell --> UnitVM
    SubjectViewCell --> UnitsManagerVM

    UnitsManagerVM --> "*" UnitVM
    UnitsManagerVM --> UnitsManager
    UnitVM --> "*" SubjectVM
    UnitVM --> Unit
    SubjectVM --> Subject

As a whole

It might be useful to note that, just like UnitVMs aggregate SubjectVMs, Units aggregate Subjects, but these relationship between Model entities were removed from the diagram above for clarity. The same is true with the View-related classes.

Here is the diagram with those relationships depicted, and the local persistence solution added.

classDiagram

    class MainView
    class UnitView
    class SubjectViewCell

    class UnitsManagerVM {
        -original: UnitsManager
        +load()
        +model: UnitsManager.Data
        +isEdited: Bool
        +isAllEditable: Bool
        +updateUnit(unitVM: UnitVM)
        +TotalAverage: Double?
        +ProfessionalAverage: Double?
    }
    class UnitVM {
        -original: Unit
        +model: Unit.Data
        +isEdited: Bool
        +onEditing()
        +onEdited(isCancelled: Bool)
        +updateSubject(subjectVM: SubjectVM)
        +updateAllSubjects()
        +deleteSubject(subjectVM: SubjectVM)
        +addSubject(subject: Subject)
        +Average: Double?
    }
    class SubjectVM {
        -original: Subject
        +model: Subject.Data
        +isEdited: Bool
        +onEditing()
        +onEdited(isCancelled: Bool)
    }


    class UnitsManager {
        -store: UnitsStore
        +save()
        +load()
        +getTotalAverage(): Double?
        +getProfessionalAverage(): Double?
        +getAverage(units: Unit[]): Double?
        +data: Data
        +update(from: Data)
    }
    class Unit {
        +name: String
        +weight: Int
        +isProfessional: Bool
        +code: Int
        +getAverage(): Double?
        +data: Data
        +update(from: Data)
    }
    class Subject {
        +name: String
        +weight: Int
        +grade: Double?
        +gradeIsValid(grade: Double?): Bool
        +data: Data
        +update(from: Data)
    }
    
    class UnitsStore {
        +load<T: Codable>(defaultValue: T[])
        +save<T: Codable>(elements: T[])
    }

    MainView --> "*" UnitView
    MainView --> UnitsManagerVM
    UnitView --> "*" SubjectViewCell
    UnitView --> UnitVM
    UnitView --> UnitsManagerVM
    SubjectViewCell --> SubjectVM
    SubjectViewCell --> UnitVM
    SubjectViewCell --> UnitsManagerVM

    UnitsManagerVM --> "*" UnitVM
    UnitsManagerVM --> UnitsManager
    UnitVM --> "*" SubjectVM
    UnitVM --> Unit
    SubjectVM --> Subject

    UnitsManager --> "*" Unit
    UnitsManager --> UnitsStore
    UnitsManager --> Stub
    Stub --> "*" Unit
    Unit --> "*" Subject

About

A SwiftUI "MVVM" graduation calculator app for educational purposes

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages