I've been trying to implement a user class that is called by multiple view to handle various classes to enable exercise and achievement unlocking wherever a certain condition is met. My code currently publishes the xp to all the UI using from the user class, but any changes happening on exercise or achievement class don't get reflected on the multiples views until my app is reloaded with the saved data. How I can fix this issue, or what is the correct approach to these cases? Thanks!
Note: The user class is passed as an Environment Object to all views.
User Class
import SwiftUI
@MainActor class User: ObservableObject {
@Published var exercises: Exercises
@Published var achievements: Achievements
@Published private (set) var XP: Int
// add last date logged
let savePath = FileManager.documentsDirectory.appendingPathComponent(Keys.userXP)
func checkIfEligibleForNewExercise() {
if exercises.isDailyExerciseAlreadyDone {
exercises.unlockNewExercise()
}
}
func markAsDone(_ exercise: Exercise) {
objectWillChange.send()
XP += exercise.xp
exercises.markAsDone(exercise)
achievements.checkIfAnyAchievementIsAchievableWith(XP)
checkIfEligibleForNewExercise()
save()
}
init() {
do {
exercises = Exercises()
achievements = Achievements()
let data = try Data(contentsOf: savePath)
XP = try JSONDecoder().decode(Int.self, from: data)
} catch {
XP = 0
save()
}
}
func save() {
do {
let encodedXP = try JSONEncoder().encode(XP)
try encodedXP.write(to: savePath, options: [.atomicWrite, .completeFileProtection])
} catch {
fatalError("Error saving XP.")
}
}
}
Exercises Class
import SwiftUI
import Foundation
class Exercise: Codable, Equatable, Identifiable {
var id = UUID()
let name: String
var isUnlocked: Bool
var type: ExerciseType
let description: String
var timesDoneToday = 0
var icon: String {
name + " icon"
}
enum ExerciseType: Codable {
case basic, advanced
}
var xp: Int {
switch type {
case .basic:
return 5
case .advanced:
return 10
}
}
init(id: UUID = UUID(), name: String, isUnlocked: Bool, type: ExerciseType, description: String) {
self.id = id
self.name = name
self.isUnlocked = isUnlocked
self.type = type
self.description = description
}
static func == (lhs: Exercise, rhs: Exercise) -> Bool {
lhs.id == rhs.id
}
static var example: Exercise {
Exercise(name: "Test Exercise", isUnlocked: true, type: .basic, description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Bibendum arcu vitae elementum curabitur vitae nunc sed. Sit amet cursus sit amet dictum sit. Habitant morbi tristique senectus et. Nunc consequat interdum varius sit amet mattis. Arcu cursus euismod quis viverra nibh. Nisl purus in mollis nunc sed id. Auctor urna nunc id cursus metus aliquam. Dolor purus non enim praesent elementum facilisis leo. Cras semper auctor neque vitae tempus quam pellentesque nec nam. Convallis tellus id interdum velit laoreet id donec. Sed viverra tellus in hac habitasse. Odio ut sem nulla pharetra diam sit amet nisl suscipit. Id ornare arcu odio ut sem nulla. Proin fermentum leo vel orci porta non pulvinar. Feugiat scelerisque varius morbi enim nunc faucibus a pellentesque. A arcu cursus vitae congue mauris rhoncus aenean vel. Nunc faucibus a pellentesque sit. Vel quam elementum pulvinar etiam. Est ultricies integer quis auctor elit sed. Lacus sed viverra tellus in hac. Mi ipsum faucibus vitae aliquet nec ullamcorper sit amet. Purus non enim praesent elementum facilisis leo vel. Vitae ultricies leo integer malesuada nunc vel risus commodo. Mollis aliquam ut porttitor leo a. Nisl vel pretium lectus quam id.")
}
}
@MainActor class Exercises: ObservableObject {
@Published private(set) var exercises: [Exercise]
let savePath = FileManager.documentsDirectory.appendingPathComponent(Keys.exercises)
init() {
do {
let data = try Data(contentsOf: savePath)
exercises = try JSONDecoder().decode([Exercise].self, from: data)
} catch {
exercises = Exercises.fillExercises()
save()
}
}
var available: [Exercise] {
exercises.filter { $0.isUnlocked }
}
var isDailyExerciseAlreadyDone: Bool {
var exercisesDone = 0
for exercise in exercises {
if exercise.timesDoneToday != 0 {
exercisesDone += exercise.timesDoneToday
}
}
print("Exercises Done Today: \(exercisesDone)")
return exercisesDone == 3 ? true : false
}
func markIsUnlocked(exercise: Exercise){
guard let index = exercises.firstIndex(of: exercise) else {
fatalError("Couldn't find the exercises")
}
objectWillChange.send()
exercises[index].isUnlocked = true
}
func unlockNewExercise() {
let exercisesLocked = exercises.filter {
!$0.isUnlocked
}
if !exercisesLocked.isEmpty {
markIsUnlocked(exercise: exercisesLocked.randomElement()!)
print("Unlocked New exercise!")
}
}
func markAsDone(_ exercise: Exercise) {
guard let index = exercises.firstIndex(of: exercise) else {
fatalError("Exercise not found")
}
print(exercises[index].timesDoneToday)
objectWillChange.send()
exercises[index].timesDoneToday += 1
print(exercises[index].timesDoneToday)
save()
}
func resetExercisesTimeDoneToday() {
for exercise in exercises {
exercise.timesDoneToday = 0
print(exercise.timesDoneToday)
}
objectWillChange.send()
save()
}
func save() {
do {
let encodedExercises = try JSONEncoder().encode(exercises)
try encodedExercises.write(to: savePath, options: [.atomicWrite, .completeFileProtection])
} catch {
fatalError("Error saving Exercises")
}
}
}
Achievements Class
import SwiftUI
class Achievement: Codable, Equatable, Identifiable {
var id = UUID()
let name: String
let description: String
var isAchieved: Bool
var dateAchieved: Date?
let xpRequired: Int
let type: AchievementType
var symbol: String {
switch type {
case .journeyStarter:
return "backpack"
case .bronze, .silver, .gold:
return "medal"
case .diamond:
return "diamond"
case .sapphire:
return "diamond.lefthalf.filled"
case .platinum:
return "diamond.inset.filled"
}
}
var symbolColor: Color {
switch type {
case .journeyStarter:
return .red
case .bronze:
return .brown
case .silver:
return .gray
case .gold:
return .yellow
case .diamond, .sapphire, .platinum:
return .cyan
}
}
init(id: UUID = UUID(), name: String, description: String, isAchieved: Bool, dateAchieved: Date? = nil, xpRequired: Int, type: AchievementType) {
self.id = id
self.name = name
self.description = description
self.isAchieved = isAchieved
self.dateAchieved = dateAchieved
self.xpRequired = xpRequired
self.type = type
}
enum AchievementType: Codable {
case journeyStarter, bronze, silver, gold, diamond ,sapphire, platinum
}
static func == (lhs: Achievement, rhs: Achievement) -> Bool {
lhs.id == rhs.id
}
var objDescription: String {
return """
id: \(id)
name: \(name)
description: \(description)
isAchieved: \(isAchieved)
"""
}
static var example: Achievement {
Achievement(name: "The Posture Checker", description: "This achievement is not real and only for testing.", isAchieved: false, xpRequired: 1000, type: .journeyStarter)
}
}
@MainActor class Achievements: ObservableObject {
@Published private(set) var achievements: [Achievement]
let savePath = FileManager.documentsDirectory.appendingPathComponent(Keys.achievements)
init() {
do {
let data = try Data(contentsOf: savePath)
achievements = try JSONDecoder().decode([Achievement].self, from: data)
} catch {
achievements = Achievements.fillAchievements()
save()
}
}
func markAsAchieved(_ achievement: Achievement) {
guard let index = achievements.firstIndex(of: achievement) else {
fatalError("Couldn't find the achievement!")
}
objectWillChange.send()
achievements[index].isAchieved = true
achievements[index].dateAchieved = Date.now
save()
}
func checkIfAnyAchievementIsAchievableWith(_ xp: Int) {
for achievement in achievements {
if achievement.xpRequired <= xp && !achievement.isAchieved {
markAsAchieved(achievement)
}
}
}
func save() {
do {
let encodedAchievements = try JSONEncoder().encode(achievements)
try encodedAchievements.write(to: savePath, options: [.atomicWrite, .completeFileProtection])
} catch {
fatalError("Error saving achievements")
}
}
}
New Code
The following code still uses Exercise and Achievement objects
import Foundation
@MainActor class ModelData: ObservableObject {
@Published private (set) var xp: Int
@Published private (set) var achievements: [Achievement]
@Published private (set) var exercises: [Exercise]
let xpSavePath = FileManager.documentsDirectory.appendingPathComponent(Keys.userXP)
let achievementsSavePath = FileManager.documentsDirectory.appendingPathComponent(Keys.achievements)
let exercisesSavePath = FileManager.documentsDirectory.appendingPathComponent(Keys.exercises)
static let shared = ModelData()
//MARK: Computed properties
// Exercises Computed properties
var isDailyExerciseAlreadyDone: Bool {
var exercisesDone = 0
for exercise in exercises {
if exercise.timesDoneToday != 0 {
exercisesDone += exercise.timesDoneToday
}
}
print("Exercises Done Today: \(exercisesDone)")
return exercisesDone == 3 ? true : false
}
var exercisesAvailable: [Exercise] {
return exercises.filter { $0.isUnlocked }
}
init() {
do {
let decoder = JSONDecoder()
let xpData = try Data(contentsOf: xpSavePath)
let achievementsData = try Data(contentsOf: achievementsSavePath)
let exercisesData = try Data(contentsOf: exercisesSavePath)
xp = try decoder.decode(Int.self, from: xpData)
achievements = try decoder.decode([Achievement].self, from: achievementsData)
exercises = try decoder.decode([Exercise].self, from: exercisesData)
print("Loaded data from disk successfully!")
} catch {
xp = 0
achievements = Constants.initialAchievements
exercises = Constants.initialExercises
save()
}
}
// MARK: Achievements methods
func checkIfAnyAchievementIsAchievable() {
for achievement in achievements {
if achievement.xpRequired <= xp && !achievement.isAchieved {
markAsAchieved(achievement)
}
}
}
func markAsAchieved(_ achievement: Achievement) {
guard let index = achievements.firstIndex(of: achievement) else { fatalError("Couldn't find achievement: \(achievement.description)")}
objectWillChange.send()
achievements[index].isAchieved = true
achievements[index].dateAchieved = Date.now
print("Achievement named: \(achievements[index].name) unlocked.")
save()
}
// MARK: Exercises methods
func unlockNewExercise() {
let exercisesLocked = exercises.filter { !$0.isUnlocked }
if !exercises.isEmpty {
markAsUnlocked(exercisesLocked.randomElement()!)
}
}
func markAsUnlocked(_ exercise: Exercise) {
guard let index = exercises.firstIndex(of: exercise) else { fatalError("Couldn't find exercise: \(exercises.description)")}
objectWillChange.send()
exercises[index].isUnlocked = true
print("Exercise named: \(exercises[index].name) unlocked.")
save()
}
func markAsDone(_ exercise: Exercise) {
guard let index = exercises.firstIndex(of: exercise) else { fatalError("Couldn't find exercise: \(exercises.description)")}
objectWillChange.send()
xp += exercise.xp
exercises[index].timesDoneToday += 1
checkIfEligibleForNewExercise()
checkIfAnyAchievementIsAchievable()
save()
}
func checkIfEligibleForNewExercise() {
if isDailyExerciseAlreadyDone {
unlockNewExercise()
}
}
func resetExercisesTimesDoneToday() {
objectWillChange.send()
for index in 0..<exercises.count {
exercises[index].timesDoneToday = 0
}
save()
}
func save() {
do {
let encoder = JSONEncoder()
let encodedXp = try encoder.encode(xp)
let encodedAchievements = try encoder.encode(achievements)
let encodedExercises = try encoder.encode(exercises)
print("Successfully encoded model properties")
try encodedXp.write(to: xpSavePath, options: [.atomicWrite, .completeFileProtection])
try encodedAchievements.write(to: achievementsSavePath, options: [.atomicWrite, .completeFileProtection])
try encodedExercises.write(to: exercisesSavePath, options: [.atomicWrite, .completeFileProtection])
print("Successfully saved model properties")
} catch {
fatalError("Could not save model properties")
}
}
}