Fixes UI iPad + diagnóstico iCloud (build 57)
Feedback del usuario en iPad: - Allocation: gráfica de tarta → semicírculo estilo hemiciclo (parlamento). SemicircleAllocation con arcos dibujados a mano (SemicircleArc Shape), segmentos por cuota en 180°, total/selección en el centro, tap por segmento. - Charts de Analyze (Compare, Period vs Period, Year vs Year) ahora SÍ muestran KPIs en la cabecera de iPad como el resto: Compare (líder/rezagado por modo), Period (retorno final por periodo), YoY (retorno final por año seleccionado). - Momentum & Streaks en el sidebar del Journal (iPad, 320pt): variante compact — tiles en fila sin subtítulos que rompían a 3 líneas, sin bloque de logros. iCloud: - CKErrorPartialFailure (CKErrorDomain code 2) mostraba un mensaje opaco. CoreDataStack.hint(for:) recorre CKPartialErrorsByItemIDKey y clasifica el código dominante → hint accionable (esquema/quota/red). Settings muestra el hint legible sobre el detalle técnico. lastSyncErrorHint publicado. Strings ×7. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
This commit is contained in:
@@ -123,6 +123,8 @@ class CoreDataStack: ObservableObject {
|
||||
@Published private(set) var lastExportDate: Date?
|
||||
@Published private(set) var isSyncing = false
|
||||
@Published private(set) var lastSyncError: String?
|
||||
/// Human-readable, actionable explanation of the last sync error (nil if none).
|
||||
@Published private(set) var lastSyncErrorHint: String?
|
||||
|
||||
var lastSyncDate: Date? { lastImportDate }
|
||||
|
||||
@@ -343,6 +345,7 @@ class CoreDataStack: ObservableObject {
|
||||
} else if let error = event.error {
|
||||
let typeLabel = event.type == .import ? "import" : event.type == .export ? "export" : "setup"
|
||||
self.lastSyncError = "\(typeLabel): \(Self.describeError(error))"
|
||||
self.lastSyncErrorHint = Self.hint(for: error)
|
||||
print("CloudKit \(typeLabel) full error:\n\(error)\nuserInfo: \((error as NSError).userInfo)")
|
||||
}
|
||||
}
|
||||
@@ -376,6 +379,44 @@ class CoreDataStack: ObservableObject {
|
||||
return parts.joined(separator: "\n")
|
||||
}
|
||||
|
||||
/// Turns an opaque CloudKit error into a human-readable, actionable hint by
|
||||
/// digging into CKPartialErrorsByItemIDKey — the per-record failures hidden
|
||||
/// inside a CKErrorPartialFailure (code 2). The dominant underlying code
|
||||
/// tells us the real problem: schema not deployed (unknownItem/invalidArguments)
|
||||
/// vs. quota vs. transient network.
|
||||
static func hint(for error: Error) -> String? {
|
||||
let ns = error as NSError
|
||||
// Collect all leaf CKError codes across a possible partial-failure tree.
|
||||
var codes: [Int] = []
|
||||
func collect(_ e: NSError) {
|
||||
if e.domain == "CKErrorDomain", e.code != 2 { codes.append(e.code) }
|
||||
if let partial = e.userInfo["CKPartialErrorsByItemIDKey"] as? [AnyHashable: Any] {
|
||||
for case let sub as NSError in partial.values { collect(sub) }
|
||||
}
|
||||
for value in e.userInfo.values {
|
||||
if let sub = value as? NSError, sub !== e { collect(sub) }
|
||||
}
|
||||
}
|
||||
collect(ns)
|
||||
|
||||
// CKError codes: 14 = unknownItem, 12 = invalidArguments, 6 = partialFailure,
|
||||
// 25 = quotaExceeded, 4 = networkFailure/unavailable, 3 = networkUnavailable,
|
||||
// 15 = serverRejectedRequest, 26 = operationCancelled.
|
||||
if codes.contains(14) || codes.contains(12) || codes.contains(15) {
|
||||
return String(localized: "icloud_hint_schema")
|
||||
}
|
||||
if codes.contains(25) {
|
||||
return String(localized: "icloud_hint_quota")
|
||||
}
|
||||
if codes.contains(3) || codes.contains(4) {
|
||||
return String(localized: "icloud_hint_network")
|
||||
}
|
||||
if ns.code == 2 {
|
||||
return String(localized: "icloud_hint_partial_generic")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func forceReload() {
|
||||
viewContext.perform { [weak self] in
|
||||
self?.viewContext.refreshAllObjects()
|
||||
|
||||
Reference in New Issue
Block a user