9927d21b86
Ambos vivían en UserDefaults (device-local, no sincroniza). Migrados a atributos CoreData en entidades ya syncable=YES, así que ahora sincronizan por CloudKit: - InvestmentSource.monthlyContribution (Decimal, opcional) - Category.allocationTarget (Double, opcional) Los stores mantienen su API por UUID (0 cambios en call sites) pero ahora leen/escriben del atributo CoreData vía fetch por id. migrateIfNeeded(context:) mueve una sola vez los valores del diccionario UserDefaults legacy al modelo y borra la clave; enganchado en CoreDataStack.loadPersistentStores junto a MonthlyCheckInStore. DashboardLayoutStore se mantiene en UserDefaults a propósito (preferencia de UI por dispositivo: el columnSpan solo aplica en iPad). NOTA: requiere desplegar el schema CloudKit a producción (CD_monthlyContribution, CD_allocationTarget) antes de que sincronice en TestFlight/App Store. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015qUZrBusG82T37R7PeokqJ
66 lines
2.6 KiB
Swift
66 lines
2.6 KiB
Swift
import Foundation
|
|
import CoreData
|
|
|
|
/// Per-category allocation target (percentage).
|
|
///
|
|
/// Backed by the `Category.allocationTarget` Core Data attribute so targets sync across
|
|
/// devices via iCloud/CloudKit. Previously stored in `UserDefaults`, which is device-local
|
|
/// and did NOT sync — `migrateIfNeeded(context:)` moves any legacy values over.
|
|
enum AllocationTargetStore {
|
|
/// Legacy UserDefaults key (pre-iCloud). Only read once during migration.
|
|
private static let legacyKey = "allocationTargets"
|
|
|
|
private static var viewContext: NSManagedObjectContext { CoreDataStack.shared.viewContext }
|
|
|
|
static func target(for categoryId: UUID) -> Double? {
|
|
guard let category = fetchCategory(categoryId),
|
|
let target = category.allocationTarget?.doubleValue,
|
|
target > 0 else { return nil }
|
|
return target
|
|
}
|
|
|
|
static func setTarget(_ value: Double?, for categoryId: UUID) {
|
|
guard let category = fetchCategory(categoryId) else { return }
|
|
if let value, value > 0 {
|
|
category.allocationTarget = NSNumber(value: value)
|
|
} else {
|
|
category.allocationTarget = nil
|
|
}
|
|
try? viewContext.save()
|
|
}
|
|
|
|
static func totalTargetPercentage(for categoryIds: [UUID]) -> Double {
|
|
categoryIds.reduce(0) { total, id in
|
|
total + (target(for: id) ?? 0)
|
|
}
|
|
}
|
|
|
|
private static func fetchCategory(_ id: UUID) -> Category? {
|
|
let request = Category.fetchRequest()
|
|
request.predicate = NSPredicate(format: "id == %@", id as CVarArg)
|
|
request.fetchLimit = 1
|
|
return try? viewContext.fetch(request).first
|
|
}
|
|
|
|
// MARK: - Migration
|
|
|
|
/// One-time migration of legacy UserDefaults targets into Core Data so they sync via
|
|
/// iCloud. Runs on every launch but is a no-op once the legacy key is cleared.
|
|
static func migrateIfNeeded(context: NSManagedObjectContext) {
|
|
guard let data = UserDefaults.standard.data(forKey: legacyKey),
|
|
let dict = try? JSONDecoder().decode([String: Double].self, from: data),
|
|
!dict.isEmpty else { return }
|
|
|
|
let categories = (try? context.fetch(Category.fetchRequest())) ?? []
|
|
var changed = false
|
|
for category in categories where category.allocationTarget == nil {
|
|
if let value = dict[category.id.uuidString], value > 0 {
|
|
category.allocationTarget = NSNumber(value: value)
|
|
changed = true
|
|
}
|
|
}
|
|
if changed { try? context.save() }
|
|
UserDefaults.standard.removeObject(forKey: legacyKey)
|
|
}
|
|
}
|