Build 21
This commit is contained in:
@@ -465,8 +465,9 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 20;
|
CURRENT_PROJECT_VERSION = 21;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
|
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = OpenClimb/Info.plist;
|
INFOPLIST_FILE = OpenClimb/Info.plist;
|
||||||
@@ -485,6 +486,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 15.6;
|
||||||
MARKETING_VERSION = 1.4.0;
|
MARKETING_VERSION = 1.4.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -495,8 +497,11 @@
|
|||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
TVOS_DEPLOYMENT_TARGET = 18.6;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||||
|
XROS_DEPLOYMENT_TARGET = 2.6;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@@ -508,8 +513,9 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 20;
|
CURRENT_PROJECT_VERSION = 21;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
|
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = OpenClimb/Info.plist;
|
INFOPLIST_FILE = OpenClimb/Info.plist;
|
||||||
@@ -528,6 +534,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 15.6;
|
||||||
MARKETING_VERSION = 1.4.0;
|
MARKETING_VERSION = 1.4.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -538,8 +545,11 @@
|
|||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 6.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
TVOS_DEPLOYMENT_TARGET = 18.6;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||||
|
XROS_DEPLOYMENT_TARGET = 2.6;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@@ -592,7 +602,7 @@
|
|||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 20;
|
CURRENT_PROJECT_VERSION = 21;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
||||||
@@ -622,7 +632,7 @@
|
|||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 20;
|
CURRENT_PROJECT_VERSION = 21;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
||||||
|
|||||||
Binary file not shown.
@@ -50,6 +50,8 @@ struct ContentView: View {
|
|||||||
Task {
|
Task {
|
||||||
try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds
|
try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds
|
||||||
dataManager.onAppBecomeActive()
|
dataManager.onAppBecomeActive()
|
||||||
|
// Re-verify health integration when app becomes active
|
||||||
|
await dataManager.healthKitService.verifyAndRestoreIntegration()
|
||||||
}
|
}
|
||||||
} else if newPhase == .background {
|
} else if newPhase == .background {
|
||||||
dataManager.onAppEnterBackground()
|
dataManager.onAppEnterBackground()
|
||||||
@@ -59,6 +61,10 @@ struct ContentView: View {
|
|||||||
setupNotificationObservers()
|
setupNotificationObservers()
|
||||||
// Trigger auto-sync on app start only
|
// Trigger auto-sync on app start only
|
||||||
dataManager.syncService.triggerAutoSync(dataManager: dataManager)
|
dataManager.syncService.triggerAutoSync(dataManager: dataManager)
|
||||||
|
// Verify and restore health integration if it was previously enabled
|
||||||
|
Task {
|
||||||
|
await dataManager.healthKitService.verifyAndRestoreIntegration()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
removeNotificationObservers()
|
removeNotificationObservers()
|
||||||
@@ -90,6 +96,8 @@ struct ContentView: View {
|
|||||||
// Small delay to ensure app is fully active
|
// Small delay to ensure app is fully active
|
||||||
try? await Task.sleep(nanoseconds: 800_000_000) // 0.8 seconds
|
try? await Task.sleep(nanoseconds: 800_000_000) // 0.8 seconds
|
||||||
await dataManager.onAppBecomeActive()
|
await dataManager.onAppBecomeActive()
|
||||||
|
// Re-verify health integration when returning from background
|
||||||
|
await dataManager.healthKitService.verifyAndRestoreIntegration()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +111,8 @@ struct ContentView: View {
|
|||||||
Task {
|
Task {
|
||||||
try? await Task.sleep(nanoseconds: 300_000_000) // 0.3 seconds
|
try? await Task.sleep(nanoseconds: 300_000_000) // 0.3 seconds
|
||||||
await dataManager.onAppBecomeActive()
|
await dataManager.onAppBecomeActive()
|
||||||
|
// Ensure health integration is verified
|
||||||
|
await dataManager.healthKitService.verifyAndRestoreIntegration()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import ActivityKit
|
import ActivityKit
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct SessionActivityAttributes: ActivityAttributes {
|
struct SessionActivityAttributes: ActivityAttributes, Sendable {
|
||||||
public struct ContentState: Codable, Hashable {
|
public struct ContentState: Codable, Hashable, Sendable {
|
||||||
var elapsed: TimeInterval
|
var elapsed: TimeInterval
|
||||||
var totalAttempts: Int
|
var totalAttempts: Int
|
||||||
var completedProblems: Int
|
var completedProblems: Int
|
||||||
@@ -17,4 +17,3 @@ extension SessionActivityAttributes {
|
|||||||
SessionActivityAttributes(gymName: "Summit Climbing", startTime: Date())
|
SessionActivityAttributes(gymName: "Summit Climbing", startTime: Date())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,9 +15,12 @@ class HealthKitService: ObservableObject {
|
|||||||
|
|
||||||
private let userDefaults = UserDefaults.standard
|
private let userDefaults = UserDefaults.standard
|
||||||
private let isEnabledKey = "healthKitEnabled"
|
private let isEnabledKey = "healthKitEnabled"
|
||||||
|
private let workoutStartDateKey = "healthKitWorkoutStartDate"
|
||||||
|
private let workoutSessionIdKey = "healthKitWorkoutSessionId"
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
loadSettings()
|
loadSettings()
|
||||||
|
restoreActiveWorkout()
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSettings() {
|
func loadSettings() {
|
||||||
@@ -28,6 +31,62 @@ class HealthKitService: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Restore active workout state
|
||||||
|
private func restoreActiveWorkout() {
|
||||||
|
if let startDate = userDefaults.object(forKey: workoutStartDateKey) as? Date,
|
||||||
|
let sessionIdString = userDefaults.string(forKey: workoutSessionIdKey),
|
||||||
|
let sessionId = UUID(uuidString: sessionIdString)
|
||||||
|
{
|
||||||
|
currentWorkoutStartDate = startDate
|
||||||
|
currentWorkoutSessionId = sessionId
|
||||||
|
print("HealthKit: Restored active workout from \(startDate)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persist active workout state
|
||||||
|
private func persistActiveWorkout() {
|
||||||
|
if let startDate = currentWorkoutStartDate, let sessionId = currentWorkoutSessionId {
|
||||||
|
userDefaults.set(startDate, forKey: workoutStartDateKey)
|
||||||
|
userDefaults.set(sessionId.uuidString, forKey: workoutSessionIdKey)
|
||||||
|
} else {
|
||||||
|
userDefaults.removeObject(forKey: workoutStartDateKey)
|
||||||
|
userDefaults.removeObject(forKey: workoutSessionIdKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify and restore health integration
|
||||||
|
func verifyAndRestoreIntegration() async {
|
||||||
|
guard isEnabled else { return }
|
||||||
|
|
||||||
|
guard HKHealthStore.isHealthDataAvailable() else {
|
||||||
|
print("HealthKit: Device does not support HealthKit")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAuthorization()
|
||||||
|
|
||||||
|
if !isAuthorized {
|
||||||
|
print(
|
||||||
|
"HealthKit: Integration was enabled but authorization lost, attempting to restore..."
|
||||||
|
)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await requestAuthorization()
|
||||||
|
print("HealthKit: Authorization restored successfully")
|
||||||
|
} catch {
|
||||||
|
print("HealthKit: Failed to restore authorization: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print("HealthKit: Integration verified - authorization is valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasActiveWorkout() {
|
||||||
|
print(
|
||||||
|
"HealthKit: Active workout restored - started at \(currentWorkoutStartDate!)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func setEnabled(_ enabled: Bool) {
|
func setEnabled(_ enabled: Bool) {
|
||||||
isEnabled = enabled
|
isEnabled = enabled
|
||||||
userDefaults.set(enabled, forKey: isEnabledKey)
|
userDefaults.set(enabled, forKey: isEnabledKey)
|
||||||
@@ -73,6 +132,8 @@ class HealthKitService: ObservableObject {
|
|||||||
|
|
||||||
currentWorkoutStartDate = startDate
|
currentWorkoutStartDate = startDate
|
||||||
currentWorkoutSessionId = sessionId
|
currentWorkoutSessionId = sessionId
|
||||||
|
persistActiveWorkout()
|
||||||
|
print("HealthKit: Started workout for session \(sessionId)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func endWorkout(endDate: Date) async throws {
|
func endWorkout(endDate: Date) async throws {
|
||||||
@@ -93,26 +154,45 @@ class HealthKitService: ObservableObject {
|
|||||||
|
|
||||||
let energyBurned = HKQuantity(unit: .kilocalorie(), doubleValue: calories)
|
let energyBurned = HKQuantity(unit: .kilocalorie(), doubleValue: calories)
|
||||||
|
|
||||||
let workout = HKWorkout(
|
let workoutConfiguration = HKWorkoutConfiguration()
|
||||||
activityType: .climbing,
|
workoutConfiguration.activityType = .climbing
|
||||||
start: startDate,
|
workoutConfiguration.locationType = .indoor
|
||||||
end: endDate,
|
|
||||||
duration: duration,
|
let builder = HKWorkoutBuilder(
|
||||||
totalEnergyBurned: energyBurned,
|
healthStore: healthStore,
|
||||||
totalDistance: nil,
|
configuration: workoutConfiguration,
|
||||||
metadata: [
|
device: .local()
|
||||||
HKMetadataKeyIndoorWorkout: true
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try await healthStore.save(workout)
|
try await builder.beginCollection(at: startDate)
|
||||||
|
|
||||||
|
let energyBurnedType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!
|
||||||
|
let energySample = HKQuantitySample(
|
||||||
|
type: energyBurnedType,
|
||||||
|
quantity: energyBurned,
|
||||||
|
start: startDate,
|
||||||
|
end: endDate
|
||||||
|
)
|
||||||
|
try await builder.addSamples([energySample])
|
||||||
|
|
||||||
|
try await builder.addMetadata([HKMetadataKeyIndoorWorkout: true])
|
||||||
|
|
||||||
|
try await builder.endCollection(at: endDate)
|
||||||
|
let workout = try await builder.finishWorkout()
|
||||||
|
|
||||||
|
print(
|
||||||
|
"HealthKit: Workout saved successfully with id: \(workout?.uuid.uuidString ?? "unknown")"
|
||||||
|
)
|
||||||
|
|
||||||
currentWorkoutStartDate = nil
|
currentWorkoutStartDate = nil
|
||||||
currentWorkoutSessionId = nil
|
currentWorkoutSessionId = nil
|
||||||
|
persistActiveWorkout()
|
||||||
} catch {
|
} catch {
|
||||||
|
print("HealthKit: Failed to save workout: \(error.localizedDescription)")
|
||||||
currentWorkoutStartDate = nil
|
currentWorkoutStartDate = nil
|
||||||
currentWorkoutSessionId = nil
|
currentWorkoutSessionId = nil
|
||||||
|
persistActiveWorkout()
|
||||||
|
|
||||||
throw HealthKitError.workoutSaveFailed
|
throw HealthKitError.workoutSaveFailed
|
||||||
}
|
}
|
||||||
@@ -121,6 +201,8 @@ class HealthKitService: ObservableObject {
|
|||||||
func cancelWorkout() {
|
func cancelWorkout() {
|
||||||
currentWorkoutStartDate = nil
|
currentWorkoutStartDate = nil
|
||||||
currentWorkoutSessionId = nil
|
currentWorkoutSessionId = nil
|
||||||
|
persistActiveWorkout()
|
||||||
|
print("HealthKit: Workout cancelled")
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasActiveWorkout() -> Bool {
|
func hasActiveWorkout() -> Bool {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
private let sharedUserDefaults = UserDefaults(suiteName: "group.com.atridad.OpenClimb")
|
private let sharedUserDefaults = UserDefaults(suiteName: "group.com.atridad.OpenClimb")
|
||||||
private let encoder = JSONEncoder()
|
private let encoder = JSONEncoder()
|
||||||
private let decoder = JSONDecoder()
|
private let decoder = JSONDecoder()
|
||||||
private var liveActivityObserver: NSObjectProtocol?
|
nonisolated(unsafe) private var liveActivityObserver: NSObjectProtocol?
|
||||||
|
|
||||||
let syncService = SyncService()
|
let syncService = SyncService()
|
||||||
let healthKitService = HealthKitService.shared
|
let healthKitService = HealthKitService.shared
|
||||||
|
|||||||
@@ -10,14 +10,10 @@ final class LiveActivityManager {
|
|||||||
static let shared = LiveActivityManager()
|
static let shared = LiveActivityManager()
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
private var currentActivity: Activity<SessionActivityAttributes>?
|
nonisolated(unsafe) private var currentActivity: Activity<SessionActivityAttributes>?
|
||||||
private var healthCheckTimer: Timer?
|
private var healthCheckTimer: Timer?
|
||||||
private var lastHealthCheck: Date = Date()
|
private var lastHealthCheck: Date = Date()
|
||||||
|
|
||||||
deinit {
|
|
||||||
healthCheckTimer?.invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if there's an active session and restart Live Activity if needed
|
/// Check if there's an active session and restart Live Activity if needed
|
||||||
func restartLiveActivityIfNeeded(activeSession: ClimbSession?, gymName: String?) async {
|
func restartLiveActivityIfNeeded(activeSession: ClimbSession?, gymName: String?) async {
|
||||||
// If we have an active session but no Live Activity, restart it
|
// If we have an active session but no Live Activity, restart it
|
||||||
@@ -130,7 +126,8 @@ final class LiveActivityManager {
|
|||||||
completedProblems: completedProblems
|
completedProblems: completedProblems
|
||||||
)
|
)
|
||||||
|
|
||||||
await currentActivity.update(.init(state: updatedContentState, staleDate: nil))
|
nonisolated(unsafe) let activity = currentActivity
|
||||||
|
await activity.update(.init(state: updatedContentState, staleDate: nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Call this when a ClimbSession ends to end the Live Activity
|
/// Call this when a ClimbSession ends to end the Live Activity
|
||||||
@@ -141,7 +138,8 @@ final class LiveActivityManager {
|
|||||||
// First end the tracked activity if it exists
|
// First end the tracked activity if it exists
|
||||||
if let currentActivity {
|
if let currentActivity {
|
||||||
print("Ending tracked Live Activity: \(currentActivity.id)")
|
print("Ending tracked Live Activity: \(currentActivity.id)")
|
||||||
await currentActivity.end(nil, dismissalPolicy: .immediate)
|
nonisolated(unsafe) let activity = currentActivity
|
||||||
|
await activity.end(nil, dismissalPolicy: .immediate)
|
||||||
self.currentActivity = nil
|
self.currentActivity = nil
|
||||||
print("Tracked Live Activity ended successfully")
|
print("Tracked Live Activity ended successfully")
|
||||||
}
|
}
|
||||||
@@ -200,7 +198,7 @@ final class LiveActivityManager {
|
|||||||
print("🩺 Starting Live Activity health checks")
|
print("🩺 Starting Live Activity health checks")
|
||||||
healthCheckTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) {
|
healthCheckTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) {
|
||||||
[weak self] _ in
|
[weak self] _ in
|
||||||
Task { @MainActor in
|
Task { @MainActor [weak self] in
|
||||||
await self?.performHealthCheck()
|
await self?.performHealthCheck()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,7 +255,7 @@ final class LiveActivityManager {
|
|||||||
{
|
{
|
||||||
guard currentActivity != nil else { return }
|
guard currentActivity != nil else { return }
|
||||||
|
|
||||||
Task {
|
Task { @MainActor in
|
||||||
while currentActivity != nil {
|
while currentActivity != nil {
|
||||||
let elapsed = Date().timeIntervalSince(session.startTime ?? session.date)
|
let elapsed = Date().timeIntervalSince(session.startTime ?? session.date)
|
||||||
await updateLiveActivity(
|
await updateLiveActivity(
|
||||||
|
|||||||
@@ -32,9 +32,19 @@ struct ProblemsView: View {
|
|||||||
filtered = filtered.filter { $0.gymId == gym.id }
|
filtered = filtered.filter { $0.gymId == gym.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separate active and inactive problems
|
// Separate active and inactive problems with stable sorting
|
||||||
let active = filtered.filter { $0.isActive }.sorted { $0.updatedAt > $1.updatedAt }
|
let active = filtered.filter { $0.isActive }.sorted {
|
||||||
let inactive = filtered.filter { !$0.isActive }.sorted { $0.updatedAt > $1.updatedAt }
|
if $0.updatedAt == $1.updatedAt {
|
||||||
|
return $0.id.uuidString < $1.id.uuidString // Stable fallback
|
||||||
|
}
|
||||||
|
return $0.updatedAt > $1.updatedAt
|
||||||
|
}
|
||||||
|
let inactive = filtered.filter { !$0.isActive }.sorted {
|
||||||
|
if $0.updatedAt == $1.updatedAt {
|
||||||
|
return $0.id.uuidString < $1.id.uuidString // Stable fallback
|
||||||
|
}
|
||||||
|
return $0.updatedAt > $1.updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
return active + inactive
|
return active + inactive
|
||||||
}
|
}
|
||||||
@@ -261,7 +271,7 @@ struct ProblemsList: View {
|
|||||||
@State private var problemToEdit: Problem?
|
@State private var problemToEdit: Problem?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List(problems) { problem in
|
List(problems, id: \.id) { problem in
|
||||||
NavigationLink(destination: ProblemDetailView(problemId: problem.id)) {
|
NavigationLink(destination: ProblemDetailView(problemId: problem.id)) {
|
||||||
ProblemRow(problem: problem)
|
ProblemRow(problem: problem)
|
||||||
}
|
}
|
||||||
@@ -273,8 +283,12 @@ struct ProblemsList: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
let updatedProblem = problem.updated(isActive: !problem.isActive)
|
// Use a spring animation for more natural movement
|
||||||
dataManager.updateProblem(updatedProblem)
|
withAnimation(.spring(response: 0.5, dampingFraction: 0.8, blendDuration: 0.1))
|
||||||
|
{
|
||||||
|
let updatedProblem = problem.updated(isActive: !problem.isActive)
|
||||||
|
dataManager.updateProblem(updatedProblem)
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label(
|
Label(
|
||||||
problem.isActive ? "Mark as Reset" : "Mark as Active",
|
problem.isActive ? "Mark as Reset" : "Mark as Active",
|
||||||
@@ -293,6 +307,14 @@ struct ProblemsList: View {
|
|||||||
.tint(.blue)
|
.tint(.blue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.animation(
|
||||||
|
.spring(response: 0.5, dampingFraction: 0.8, blendDuration: 0.1),
|
||||||
|
value: problems.map { "\($0.id):\($0.isActive)" }.joined()
|
||||||
|
)
|
||||||
|
.listStyle(.plain)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.scrollIndicators(.hidden)
|
||||||
|
.clipped()
|
||||||
.alert("Delete Problem", isPresented: .constant(problemToDelete != nil)) {
|
.alert("Delete Problem", isPresented: .constant(problemToDelete != nil)) {
|
||||||
Button("Cancel", role: .cancel) {
|
Button("Cancel", role: .cancel) {
|
||||||
problemToDelete = nil
|
problemToDelete = nil
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import ActivityKit
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|
||||||
struct SessionActivityAttributes: ActivityAttributes {
|
struct SessionActivityAttributes: ActivityAttributes, Sendable {
|
||||||
public struct ContentState: Codable, Hashable {
|
public struct ContentState: Codable, Hashable, Sendable {
|
||||||
var elapsed: TimeInterval
|
var elapsed: TimeInterval
|
||||||
var totalAttempts: Int
|
var totalAttempts: Int
|
||||||
var completedProblems: Int
|
var completedProblems: Int
|
||||||
|
|||||||
Reference in New Issue
Block a user