Proper 1.0 release for iOS. Pending App Store submission.

This commit is contained in:
2025-09-15 21:01:02 -06:00
parent d95c45abbb
commit afd954785a
24 changed files with 1848 additions and 2 deletions

View File

@@ -3,6 +3,10 @@ import Foundation
import SwiftUI
import UniformTypeIdentifiers
#if canImport(WidgetKit)
import WidgetKit
#endif
@MainActor
class ClimbingDataManager: ObservableObject {
@@ -16,6 +20,7 @@ class ClimbingDataManager: ObservableObject {
@Published var successMessage: String?
private let userDefaults = UserDefaults.standard
private let sharedUserDefaults = UserDefaults(suiteName: "group.com.atridad.OpenClimb")
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
@@ -35,6 +40,9 @@ class ClimbingDataManager: ObservableObject {
Task {
try? await Task.sleep(nanoseconds: 2_000_000_000)
await performImageMaintenance()
// Check if we need to restart Live Activity for active session
await checkAndRestartLiveActivity()
}
}
@@ -89,24 +97,34 @@ class ClimbingDataManager: ObservableObject {
private func saveGyms() {
if let data = try? encoder.encode(gyms) {
userDefaults.set(data, forKey: Keys.gyms)
// Share with widget
sharedUserDefaults?.set(data, forKey: Keys.gyms)
}
}
private func saveProblems() {
if let data = try? encoder.encode(problems) {
userDefaults.set(data, forKey: Keys.problems)
// Share with widget
sharedUserDefaults?.set(data, forKey: Keys.problems)
}
}
private func saveSessions() {
if let data = try? encoder.encode(sessions) {
userDefaults.set(data, forKey: Keys.sessions)
// Share with widget
sharedUserDefaults?.set(data, forKey: Keys.sessions)
}
}
private func saveAttempts() {
if let data = try? encoder.encode(attempts) {
userDefaults.set(data, forKey: Keys.attempts)
// Share with widget
sharedUserDefaults?.set(data, forKey: Keys.attempts)
// Update widget timeline
updateWidgetTimeline()
}
}
@@ -216,6 +234,14 @@ class ClimbingDataManager: ObservableObject {
successMessage = "Session started successfully"
clearMessageAfterDelay()
// MARK: - Start Live Activity for new session
if let gym = gym(withId: gymId) {
Task {
await LiveActivityManager.shared.startLiveActivity(
for: newSession, gymName: gym.name)
}
}
}
func endSession(_ sessionId: UUID) {
@@ -234,6 +260,11 @@ class ClimbingDataManager: ObservableObject {
saveSessions()
successMessage = "Session completed successfully"
clearMessageAfterDelay()
// MARK: - End Live Activity after session ends
Task {
await LiveActivityManager.shared.endLiveActivity()
}
}
}
@@ -249,6 +280,9 @@ class ClimbingDataManager: ObservableObject {
saveSessions()
successMessage = "Session updated successfully"
clearMessageAfterDelay()
// Update Live Activity when session updates
updateLiveActivityForActiveSession()
}
}
@@ -290,6 +324,9 @@ class ClimbingDataManager: ObservableObject {
successMessage = "Attempt logged successfully"
clearMessageAfterDelay()
// Update Live Activity when new attempt is added
updateLiveActivityForActiveSession()
}
func updateAttempt(_ attempt: Attempt) {
@@ -298,6 +335,9 @@ class ClimbingDataManager: ObservableObject {
saveAttempts()
successMessage = "Attempt updated successfully"
clearMessageAfterDelay()
// Update Live Activity when attempt is updated
updateLiveActivityForActiveSession()
}
}
@@ -306,6 +346,9 @@ class ClimbingDataManager: ObservableObject {
saveAttempts()
successMessage = "Attempt deleted successfully"
clearMessageAfterDelay()
// Update Live Activity when attempt is deleted
updateLiveActivityForActiveSession()
}
func attempts(forSession sessionId: UUID) -> [Attempt] {
@@ -924,6 +967,100 @@ extension ClimbingDataManager {
"""
}
func testLiveActivity() {
print("🧪 Testing Live Activity functionality...")
// Check Live Activity availability
let status = LiveActivityManager.shared.checkLiveActivityAvailability()
print(status)
// Test with dummy data if we have a gym
guard let testGym = gyms.first else {
print("❌ No gyms available for testing")
return
}
// Create a test session
let testSession = ClimbSession(gymId: testGym.id, notes: "Test session for Live Activity")
Task {
await LiveActivityManager.shared.startLiveActivity(
for: testSession, gymName: testGym.name)
// Wait a bit then update
try? await Task.sleep(nanoseconds: 2_000_000_000)
await LiveActivityManager.shared.updateLiveActivity(
elapsed: 120, totalAttempts: 5, completedProblems: 1)
// Wait then end
try? await Task.sleep(nanoseconds: 5_000_000_000)
await LiveActivityManager.shared.endLiveActivity()
}
}
private func checkAndRestartLiveActivity() async {
guard let activeSession = activeSession else { return }
if let gym = gym(withId: activeSession.gymId) {
await LiveActivityManager.shared.restartLiveActivityIfNeeded(
activeSession: activeSession,
gymName: gym.name
)
}
}
/// Call this when app becomes active to check for Live Activity restart
func onAppBecomeActive() {
Task {
await checkAndRestartLiveActivity()
}
}
/// Update Live Activity with current session data
private func updateLiveActivityForActiveSession() {
guard let activeSession = activeSession,
activeSession.status == .active,
let gym = gym(withId: activeSession.gymId)
else {
return
}
let attemptsForSession = attempts(forSession: activeSession.id)
let totalAttempts = attemptsForSession.count
let completedProblemIds = Set(
attemptsForSession.filter { $0.result.isSuccessful }.map { $0.problemId }
)
let completedProblems = completedProblemIds.count
let elapsedInterval: TimeInterval
if let startTime = activeSession.startTime {
elapsedInterval = Date().timeIntervalSince(startTime)
} else {
elapsedInterval = 0
}
Task {
await LiveActivityManager.shared.updateLiveActivity(
elapsed: elapsedInterval,
totalAttempts: totalAttempts,
completedProblems: completedProblems
)
}
}
/// Manually force Live Activity update (useful for debugging)
func forceLiveActivityUpdate() {
updateLiveActivityForActiveSession()
}
/// Update widget timeline when data changes
private func updateWidgetTimeline() {
#if canImport(WidgetKit)
WidgetCenter.shared.reloadTimelines(ofKind: "SessionStatusLive")
#endif
}
private func validateImportData(_ importData: ClimbDataExport) throws {
if importData.gyms.isEmpty {
throw NSError(