Hotfix: accessors nil-tolerantes en Core Data — crashes de bridging con iCloud sync (build 56)
Dos crashes de producción con la misma causa raíz: atributos declarados no-opcionales en Swift (@NSManaged id: UUID, date: Date) que llegan nil en runtime — NSPersistentCloudKitContainer materializa registros de otros dispositivos por fases y los objetos borrados pueden seguir referenciados por vistas vivas. El force-bridge de nil revienta: 1. UUID._unconditionallyBridgeFromObjectiveC en ForEach (keypath Identifiable.id) 2. Date._unconditionallyBridgeFromObjectiveC en ChartsViewModel.monthlyTotals Fix sistémico (no por call-site): NSManagedObject.safeValue(forKey:fallback:healing:) lee el primitive y cae a un valor seguro; id se auto-cura (UUID nuevo escrito al primitive sin ensuciar el objeto). Aplicado a: - Snapshot: id (heal), date, createdAt (fallback distantPast) - InvestmentSource / Goal / Account: id (heal), name (fallback "") - Category: id (heal), name, colorHex, icon (fallbacks neutros) Los accessors son @objc → NSSortDescriptor(keyPath:) y KVC siguen funcionando. awakeFromInsert/awakeFromFetch se mantienen. Smoke test UITest en verde. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
This commit is contained in:
@@ -7,8 +7,14 @@ public class Account: NSManagedObject, Identifiable {
|
||||
return NSFetchRequest<Account>(entityName: "Account")
|
||||
}
|
||||
|
||||
@NSManaged public var id: UUID
|
||||
@NSManaged public var name: String
|
||||
@objc public var id: UUID {
|
||||
get { safeValue(forKey: "id", fallback: UUID(), healing: true) }
|
||||
set { setManagedValue(newValue, forKey: "id") }
|
||||
}
|
||||
@objc public var name: String {
|
||||
get { safeValue(forKey: "name", fallback: "") }
|
||||
set { setManagedValue(newValue, forKey: "name") }
|
||||
}
|
||||
@NSManaged public var createdAt: Date
|
||||
@NSManaged public var currency: String?
|
||||
@NSManaged public var inputMode: String
|
||||
|
||||
@@ -8,10 +8,22 @@ public class Category: NSManagedObject, Identifiable {
|
||||
return NSFetchRequest<Category>(entityName: "Category")
|
||||
}
|
||||
|
||||
@NSManaged public var id: UUID
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var colorHex: String
|
||||
@NSManaged public var icon: String
|
||||
@objc public var id: UUID {
|
||||
get { safeValue(forKey: "id", fallback: UUID(), healing: true) }
|
||||
set { setManagedValue(newValue, forKey: "id") }
|
||||
}
|
||||
@objc public var name: String {
|
||||
get { safeValue(forKey: "name", fallback: "") }
|
||||
set { setManagedValue(newValue, forKey: "name") }
|
||||
}
|
||||
@objc public var colorHex: String {
|
||||
get { safeValue(forKey: "colorHex", fallback: "#6B7280") }
|
||||
set { setManagedValue(newValue, forKey: "colorHex") }
|
||||
}
|
||||
@objc public var icon: String {
|
||||
get { safeValue(forKey: "icon", fallback: "questionmark") }
|
||||
set { setManagedValue(newValue, forKey: "icon") }
|
||||
}
|
||||
@NSManaged public var sortOrder: Int16
|
||||
@NSManaged public var createdAt: Date
|
||||
@NSManaged public var allocationTarget: NSNumber?
|
||||
|
||||
@@ -7,8 +7,14 @@ public class Goal: NSManagedObject, Identifiable {
|
||||
return NSFetchRequest<Goal>(entityName: "Goal")
|
||||
}
|
||||
|
||||
@NSManaged public var id: UUID
|
||||
@NSManaged public var name: String
|
||||
@objc public var id: UUID {
|
||||
get { safeValue(forKey: "id", fallback: UUID(), healing: true) }
|
||||
set { setManagedValue(newValue, forKey: "id") }
|
||||
}
|
||||
@objc public var name: String {
|
||||
get { safeValue(forKey: "name", fallback: "") }
|
||||
set { setManagedValue(newValue, forKey: "name") }
|
||||
}
|
||||
@NSManaged public var targetAmount: NSDecimalNumber?
|
||||
@NSManaged public var targetDate: Date?
|
||||
@NSManaged public var isActive: Bool
|
||||
|
||||
@@ -7,8 +7,14 @@ public class InvestmentSource: NSManagedObject, Identifiable {
|
||||
return NSFetchRequest<InvestmentSource>(entityName: "InvestmentSource")
|
||||
}
|
||||
|
||||
@NSManaged public var id: UUID
|
||||
@NSManaged public var name: String
|
||||
@objc public var id: UUID {
|
||||
get { safeValue(forKey: "id", fallback: UUID(), healing: true) }
|
||||
set { setManagedValue(newValue, forKey: "id") }
|
||||
}
|
||||
@objc public var name: String {
|
||||
get { safeValue(forKey: "name", fallback: "") }
|
||||
set { setManagedValue(newValue, forKey: "name") }
|
||||
}
|
||||
@NSManaged public var notificationFrequency: String
|
||||
@NSManaged public var customFrequencyMonths: Int16
|
||||
@NSManaged public var isActive: Bool
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import CoreData
|
||||
|
||||
// MARK: - Nil-tolerant accessors for CloudKit-synced attributes
|
||||
//
|
||||
// Attributes declared non-optional in Swift (`id: UUID`, `date: Date`, …) CAN
|
||||
// be nil at runtime: NSPersistentCloudKitContainer merges records from other
|
||||
// devices in stages, and deleted objects may still be referenced by live views.
|
||||
// Reading them through `@NSManaged` force-bridges nil and crashes
|
||||
// (`UUID/Date._unconditionallyBridgeFromObjectiveC`) — seen in production in
|
||||
// SwiftUI ForEach identity (nil id) and ChartsViewModel.monthlyTotals (nil
|
||||
// date). These helpers read the primitive value and fall back safely.
|
||||
extension NSManagedObject {
|
||||
/// Reads a primitive attribute, returning `fallback` when nil. With
|
||||
/// `healing: true` the fallback is also written back to the primitive
|
||||
/// (without dirtying the object) so identity stays stable for the session.
|
||||
func safeValue<T>(forKey key: String, fallback: @autoclosure () -> T, healing: Bool = false) -> T {
|
||||
willAccessValue(forKey: key)
|
||||
defer { didAccessValue(forKey: key) }
|
||||
if let value = primitiveValue(forKey: key) as? T {
|
||||
return value
|
||||
}
|
||||
let healed = fallback()
|
||||
if healing {
|
||||
setPrimitiveValue(healed, forKey: key)
|
||||
}
|
||||
return healed
|
||||
}
|
||||
|
||||
func setManagedValue<T>(_ value: T, forKey key: String) {
|
||||
willChangeValue(forKey: key)
|
||||
setPrimitiveValue(value, forKey: key)
|
||||
didChangeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,21 @@ public class Snapshot: NSManagedObject, Identifiable {
|
||||
return NSFetchRequest<Snapshot>(entityName: "Snapshot")
|
||||
}
|
||||
|
||||
@NSManaged public var id: UUID
|
||||
@NSManaged public var date: Date
|
||||
@objc public var id: UUID {
|
||||
get { safeValue(forKey: "id", fallback: UUID(), healing: true) }
|
||||
set { setManagedValue(newValue, forKey: "id") }
|
||||
}
|
||||
@objc public var date: Date {
|
||||
get { safeValue(forKey: "date", fallback: .distantPast) }
|
||||
set { setManagedValue(newValue, forKey: "date") }
|
||||
}
|
||||
@NSManaged public var value: NSDecimalNumber?
|
||||
@NSManaged public var contribution: NSDecimalNumber?
|
||||
@NSManaged public var notes: String?
|
||||
@NSManaged public var createdAt: Date
|
||||
@objc public var createdAt: Date {
|
||||
get { safeValue(forKey: "createdAt", fallback: .distantPast) }
|
||||
set { setManagedValue(newValue, forKey: "createdAt") }
|
||||
}
|
||||
@NSManaged public var source: InvestmentSource?
|
||||
|
||||
public override func awakeFromInsert() {
|
||||
|
||||
Reference in New Issue
Block a user