iOS Build 22
This commit is contained in:
@@ -96,6 +96,9 @@ class ClimbingDataManager: ObservableObject {
|
||||
loadSessions()
|
||||
loadAttempts()
|
||||
loadActiveSession()
|
||||
|
||||
// Clean up orphaned data after loading
|
||||
cleanupOrphanedData()
|
||||
}
|
||||
|
||||
private func loadGyms() {
|
||||
@@ -286,7 +289,16 @@ class ClimbingDataManager: ObservableObject {
|
||||
}
|
||||
|
||||
func deleteProblem(_ problem: Problem) {
|
||||
// Delete associated attempts first
|
||||
// Track deletion of the problem
|
||||
trackDeletion(itemId: problem.id.uuidString, itemType: "problem")
|
||||
|
||||
// Find and track all attempts for this problem as deleted
|
||||
let problemAttempts = attempts.filter { $0.problemId == problem.id }
|
||||
for attempt in problemAttempts {
|
||||
trackDeletion(itemId: attempt.id.uuidString, itemType: "attempt")
|
||||
}
|
||||
|
||||
// Delete associated attempts
|
||||
attempts.removeAll { $0.problemId == problem.id }
|
||||
saveAttempts()
|
||||
|
||||
@@ -295,7 +307,6 @@ class ClimbingDataManager: ObservableObject {
|
||||
|
||||
// Delete the problem
|
||||
problems.removeAll { $0.id == problem.id }
|
||||
trackDeletion(itemId: problem.id.uuidString, itemType: "problem")
|
||||
saveProblems()
|
||||
DataStateManager.shared.updateDataState()
|
||||
|
||||
@@ -410,7 +421,16 @@ class ClimbingDataManager: ObservableObject {
|
||||
}
|
||||
|
||||
func deleteSession(_ session: ClimbSession) {
|
||||
// Delete associated attempts first
|
||||
// Track deletion of the session
|
||||
trackDeletion(itemId: session.id.uuidString, itemType: "session")
|
||||
|
||||
// Find and track all attempts for this session as deleted
|
||||
let sessionAttempts = attempts.filter { $0.sessionId == session.id }
|
||||
for attempt in sessionAttempts {
|
||||
trackDeletion(itemId: attempt.id.uuidString, itemType: "attempt")
|
||||
}
|
||||
|
||||
// Delete associated attempts
|
||||
attempts.removeAll { $0.sessionId == session.id }
|
||||
saveAttempts()
|
||||
|
||||
@@ -422,7 +442,6 @@ class ClimbingDataManager: ObservableObject {
|
||||
|
||||
// Delete the session
|
||||
sessions.removeAll { $0.id == session.id }
|
||||
trackDeletion(itemId: session.id.uuidString, itemType: "session")
|
||||
saveSessions()
|
||||
DataStateManager.shared.updateDataState()
|
||||
|
||||
@@ -548,6 +567,162 @@ class ClimbingDataManager: ObservableObject {
|
||||
return gym(withId: mostUsedGymId)
|
||||
}
|
||||
|
||||
/// Clean up orphaned data - removes attempts that reference non-existent sessions
|
||||
/// and removes duplicate attempts. This ensures data integrity and prevents
|
||||
/// orphaned attempts from appearing in widgets
|
||||
private func cleanupOrphanedData() {
|
||||
let validSessionIds = Set(sessions.map { $0.id })
|
||||
let validProblemIds = Set(problems.map { $0.id })
|
||||
let validGymIds = Set(gyms.map { $0.id })
|
||||
|
||||
let initialAttemptCount = attempts.count
|
||||
|
||||
// Remove attempts that reference deleted sessions or problems
|
||||
let orphanedAttempts = attempts.filter { attempt in
|
||||
!validSessionIds.contains(attempt.sessionId)
|
||||
|| !validProblemIds.contains(attempt.problemId)
|
||||
}
|
||||
|
||||
if !orphanedAttempts.isEmpty {
|
||||
print("🧹 Cleaning up \(orphanedAttempts.count) orphaned attempts")
|
||||
|
||||
// Track these as deleted to prevent sync from re-introducing them
|
||||
for attempt in orphanedAttempts {
|
||||
trackDeletion(itemId: attempt.id.uuidString, itemType: "attempt")
|
||||
}
|
||||
|
||||
// Remove orphaned attempts
|
||||
attempts.removeAll { attempt in
|
||||
!validSessionIds.contains(attempt.sessionId)
|
||||
|| !validProblemIds.contains(attempt.problemId)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicate attempts (same session, problem, and timestamp within 1 second)
|
||||
var seenAttempts: Set<String> = []
|
||||
var duplicateIds: [UUID] = []
|
||||
|
||||
for attempt in attempts.sorted(by: { $0.timestamp < $1.timestamp }) {
|
||||
// Create a unique key based on session, problem, and rounded timestamp
|
||||
let timestampKey = Int(attempt.timestamp.timeIntervalSince1970)
|
||||
let key =
|
||||
"\(attempt.sessionId.uuidString)_\(attempt.problemId.uuidString)_\(timestampKey)"
|
||||
|
||||
if seenAttempts.contains(key) {
|
||||
duplicateIds.append(attempt.id)
|
||||
print("🧹 Found duplicate attempt: \(attempt.id)")
|
||||
} else {
|
||||
seenAttempts.insert(key)
|
||||
}
|
||||
}
|
||||
|
||||
if !duplicateIds.isEmpty {
|
||||
print("🧹 Removing \(duplicateIds.count) duplicate attempts")
|
||||
|
||||
// Track duplicates as deleted
|
||||
for attemptId in duplicateIds {
|
||||
trackDeletion(itemId: attemptId.uuidString, itemType: "attempt")
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
attempts.removeAll { duplicateIds.contains($0.id) }
|
||||
}
|
||||
|
||||
if initialAttemptCount != attempts.count {
|
||||
saveAttempts()
|
||||
let removedCount = initialAttemptCount - attempts.count
|
||||
print(
|
||||
"✅ Cleanup complete. Removed \(removedCount) attempts. Remaining: \(attempts.count)"
|
||||
)
|
||||
}
|
||||
|
||||
// Remove problems that reference deleted gyms
|
||||
let orphanedProblems = problems.filter { problem in
|
||||
!validGymIds.contains(problem.gymId)
|
||||
}
|
||||
|
||||
if !orphanedProblems.isEmpty {
|
||||
print("🧹 Cleaning up \(orphanedProblems.count) orphaned problems")
|
||||
|
||||
for problem in orphanedProblems {
|
||||
trackDeletion(itemId: problem.id.uuidString, itemType: "problem")
|
||||
}
|
||||
|
||||
problems.removeAll { problem in
|
||||
!validGymIds.contains(problem.gymId)
|
||||
}
|
||||
|
||||
saveProblems()
|
||||
}
|
||||
|
||||
// Remove sessions that reference deleted gyms
|
||||
let orphanedSessions = sessions.filter { session in
|
||||
!validGymIds.contains(session.gymId)
|
||||
}
|
||||
|
||||
if !orphanedSessions.isEmpty {
|
||||
print("🧹 Cleaning up \(orphanedSessions.count) orphaned sessions")
|
||||
|
||||
for session in orphanedSessions {
|
||||
trackDeletion(itemId: session.id.uuidString, itemType: "session")
|
||||
}
|
||||
|
||||
sessions.removeAll { session in
|
||||
!validGymIds.contains(session.gymId)
|
||||
}
|
||||
|
||||
saveSessions()
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate data integrity and return a report
|
||||
/// This can be called manually to check for issues
|
||||
func validateDataIntegrity() -> String {
|
||||
let validSessionIds = Set(sessions.map { $0.id })
|
||||
let validProblemIds = Set(problems.map { $0.id })
|
||||
let validGymIds = Set(gyms.map { $0.id })
|
||||
|
||||
let orphanedAttempts = attempts.filter { attempt in
|
||||
!validSessionIds.contains(attempt.sessionId)
|
||||
|| !validProblemIds.contains(attempt.problemId)
|
||||
}
|
||||
|
||||
let orphanedProblems = problems.filter { problem in
|
||||
!validGymIds.contains(problem.gymId)
|
||||
}
|
||||
|
||||
let orphanedSessions = sessions.filter { session in
|
||||
!validGymIds.contains(session.gymId)
|
||||
}
|
||||
|
||||
var report = "Data Integrity Report:\n"
|
||||
report += "---------------------\n"
|
||||
report += "Gyms: \(gyms.count)\n"
|
||||
report += "Problems: \(problems.count)\n"
|
||||
report += "Sessions: \(sessions.count)\n"
|
||||
report += "Attempts: \(attempts.count)\n"
|
||||
report += "\nOrphaned Data:\n"
|
||||
report += "Orphaned Attempts: \(orphanedAttempts.count)\n"
|
||||
report += "Orphaned Problems: \(orphanedProblems.count)\n"
|
||||
report += "Orphaned Sessions: \(orphanedSessions.count)\n"
|
||||
|
||||
if orphanedAttempts.isEmpty && orphanedProblems.isEmpty && orphanedSessions.isEmpty {
|
||||
report += "\n✅ No integrity issues found"
|
||||
} else {
|
||||
report += "\n⚠️ Issues found - run cleanup to fix"
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
/// Manually trigger cleanup of orphaned data
|
||||
/// This can be called from settings or debug menu
|
||||
func manualDataCleanup() {
|
||||
cleanupOrphanedData()
|
||||
successMessage = "Data cleanup completed"
|
||||
clearMessageAfterDelay()
|
||||
}
|
||||
|
||||
func resetAllData(showSuccessMessage: Bool = true) {
|
||||
gyms.removeAll()
|
||||
problems.removeAll()
|
||||
|
||||
Reference in New Issue
Block a user