iOS 1.2.1 - Better auto sync and sync indicator

This commit is contained in:
2025-09-29 13:47:50 -06:00
parent c3f847e1e6
commit b930d5ce96
15 changed files with 185 additions and 220 deletions

View File

@@ -1,6 +1,5 @@
import Combine
import Foundation
import UIKit
@MainActor
class SyncService: ObservableObject {
@@ -455,7 +454,7 @@ class SyncService: ObservableObject {
let zipData = try createMinimalZipFromBackup(updatedBackup)
// Use existing import method which properly handles data restoration
try dataManager.importData(from: zipData)
try dataManager.importData(from: zipData, showSuccessMessage: false)
// Update local data state to match imported data timestamp
DataStateManager.shared.setLastModified(backup.exportedAt)
@@ -735,180 +734,29 @@ class SyncService: ObservableObject {
}
func triggerAutoSync(dataManager: ClimbingDataManager) {
guard isConnected && isConfigured && isAutoSyncEnabled else { return }
// Early exit if sync cannot proceed - don't set isSyncing
guard isConnected && isConfigured && isAutoSyncEnabled else {
// Ensure isSyncing is false when sync is not possible
if isSyncing {
isSyncing = false
}
return
}
// Prevent multiple simultaneous syncs
guard !isSyncing else {
return
}
Task {
do {
try await syncWithServer(dataManager: dataManager)
} catch {
print("Auto-sync failed: \(error)")
// Don't show UI errors for auto-sync failures
}
}
}
// DEPRECATED: Complex merge logic replaced with simple timestamp-based sync
// These methods are no longer used but kept for reference
@available(*, deprecated, message: "Use simple timestamp-based sync instead")
private func performIntelligentMerge(local: ClimbDataBackup, server: ClimbDataBackup) throws
-> ClimbDataBackup
{
print("Merging data - preserving all entities to prevent data loss")
// Merge gyms by ID, keeping most recently updated
let mergedGyms = mergeGyms(local: local.gyms, server: server.gyms)
// Merge problems by ID, keeping most recently updated
let mergedProblems = mergeProblems(local: local.problems, server: server.problems)
// Merge sessions by ID, keeping most recently updated
let mergedSessions = mergeSessions(local: local.sessions, server: server.sessions)
// Merge attempts by ID, keeping most recently updated
let mergedAttempts = mergeAttempts(local: local.attempts, server: server.attempts)
print(
"Merge results: gyms=\(mergedGyms.count), problems=\(mergedProblems.count), sessions=\(mergedSessions.count), attempts=\(mergedAttempts.count)"
)
return ClimbDataBackup(
exportedAt: ISO8601DateFormatter().string(from: Date()),
version: "2.0",
formatVersion: "2.0",
gyms: mergedGyms,
problems: mergedProblems,
sessions: mergedSessions,
attempts: mergedAttempts
)
}
private func mergeGyms(local: [BackupGym], server: [BackupGym]) -> [BackupGym] {
var merged: [String: BackupGym] = [:]
// Add all local gyms
for gym in local {
merged[gym.id] = gym
}
// Add server gyms, replacing if newer
for serverGym in server {
if let localGym = merged[serverGym.id] {
// Keep the most recently updated
if isNewerThan(serverGym.updatedAt, localGym.updatedAt) {
merged[serverGym.id] = serverGym
await MainActor.run {
self.isSyncing = false
}
} else {
// New gym from server
merged[serverGym.id] = serverGym
}
}
return Array(merged.values)
}
private func mergeProblems(local: [BackupProblem], server: [BackupProblem]) -> [BackupProblem] {
var merged: [String: BackupProblem] = [:]
// Add all local problems
for problem in local {
merged[problem.id] = problem
}
// Add server problems, replacing if newer or merging image paths
for serverProblem in server {
if let localProblem = merged[serverProblem.id] {
// Merge image paths from both sources
let localImages = Set(localProblem.imagePaths ?? [])
let serverImages = Set(serverProblem.imagePaths ?? [])
let mergedImages = Array(localImages.union(serverImages))
// Use most recently updated problem data but with merged images
let newerProblem =
isNewerThan(serverProblem.updatedAt, localProblem.updatedAt)
? serverProblem : localProblem
merged[serverProblem.id] = BackupProblem(
id: newerProblem.id,
gymId: newerProblem.gymId,
name: newerProblem.name,
description: newerProblem.description,
climbType: newerProblem.climbType,
difficulty: newerProblem.difficulty,
tags: newerProblem.tags,
location: newerProblem.location,
imagePaths: mergedImages.isEmpty ? nil : mergedImages,
isActive: newerProblem.isActive,
dateSet: newerProblem.dateSet,
notes: newerProblem.notes,
createdAt: newerProblem.createdAt,
updatedAt: newerProblem.updatedAt
)
} else {
// New problem from server
merged[serverProblem.id] = serverProblem
}
}
return Array(merged.values)
}
private func mergeSessions(local: [BackupClimbSession], server: [BackupClimbSession])
-> [BackupClimbSession]
{
var merged: [String: BackupClimbSession] = [:]
// Add all local sessions
for session in local {
merged[session.id] = session
}
// Add server sessions, replacing if newer
for serverSession in server {
if let localSession = merged[serverSession.id] {
// Keep the most recently updated
if isNewerThan(serverSession.updatedAt, localSession.updatedAt) {
merged[serverSession.id] = serverSession
}
} else {
// New session from server
merged[serverSession.id] = serverSession
}
}
return Array(merged.values)
}
private func mergeAttempts(local: [BackupAttempt], server: [BackupAttempt]) -> [BackupAttempt] {
var merged: [String: BackupAttempt] = [:]
// Add all local attempts
for attempt in local {
merged[attempt.id] = attempt
}
// Add server attempts, replacing if newer
for serverAttempt in server {
if let localAttempt = merged[serverAttempt.id] {
// Keep the most recently created (attempts don't typically get updated)
if isNewerThan(serverAttempt.createdAt, localAttempt.createdAt) {
merged[serverAttempt.id] = serverAttempt
}
} else {
// New attempt from server
merged[serverAttempt.id] = serverAttempt
}
}
return Array(merged.values)
}
private func isNewerThan(_ dateString1: String, _ dateString2: String) -> Bool {
let formatter = ISO8601DateFormatter()
guard let date1 = formatter.date(from: dateString1),
let date2 = formatter.date(from: dateString2)
else {
return false
}
return date1 > date2
}
func disconnect() {
@@ -931,8 +779,6 @@ class SyncService: ObservableObject {
}
}
// Removed SyncTrigger enum - now using simple auto sync on any data change
enum SyncError: LocalizedError {
case notConfigured
case notConnected