Rama 1.6.0: dedup CloudKit-safe (WIP: StableID + UUIDs estables en defaults)
Funde 1.5.1 en 1.6.0. Parte 1 dedup (prevención): StableID (UUIDv5 namespace+nombre), Account.defaultAccountStableID, Category.stableID(for:) + asignación en createDefaultCategories. Pendiente: asignar en repos + limpieza de duplicados existentes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
This commit is contained in:
@@ -559,7 +559,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.5.1;
|
||||
MARKETING_VERSION = 1.6.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournal;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
@@ -600,7 +600,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.5.1;
|
||||
MARKETING_VERSION = 1.6.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournal;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
@@ -752,7 +752,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.5.1;
|
||||
MARKETING_VERSION = 1.6.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournal.PortfolioJournalWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -787,7 +787,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.5.1;
|
||||
MARKETING_VERSION = 1.6.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournal.PortfolioJournalWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -814,7 +814,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.5.1;
|
||||
MARKETING_VERSION = 1.6.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournalTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
@@ -838,7 +838,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.5.1;
|
||||
MARKETING_VERSION = 1.6.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournalTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
@@ -861,7 +861,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.5.1;
|
||||
MARKETING_VERSION = 1.6.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournalUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
@@ -884,7 +884,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.5.1;
|
||||
MARKETING_VERSION = 1.6.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournalUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
@@ -911,7 +911,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.5.1;
|
||||
MARKETING_VERSION = 1.6.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournal.QuickUpdate;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -940,7 +940,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.5.1;
|
||||
MARKETING_VERSION = 1.6.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournal.QuickUpdate;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
||||
@@ -46,6 +46,11 @@ public class Account: NSManagedObject, Identifiable {
|
||||
|
||||
extension Account {
|
||||
static let defaultAccountName = "Default"
|
||||
|
||||
/// Deterministic UUID for the auto-created "Default" account, so every device
|
||||
/// creates it as the SAME CloudKit record instead of a per-device duplicate.
|
||||
/// See StableID.
|
||||
static let defaultAccountStableID = StableID.v5("account:default")
|
||||
}
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
@@ -92,9 +92,17 @@ extension Category {
|
||||
("ETFs", "#EC4899", "chart.bar.fill")
|
||||
]
|
||||
|
||||
/// Deterministic UUID for a default category, so every device creates each
|
||||
/// default category as the SAME CloudKit record instead of a per-device
|
||||
/// duplicate. Only meaningful for the built-in default names. See StableID.
|
||||
static func stableID(for name: String) -> UUID {
|
||||
StableID.v5("category:\(name.lowercased())")
|
||||
}
|
||||
|
||||
static func createDefaultCategories(in context: NSManagedObjectContext) {
|
||||
for (index, categoryData) in defaultCategories.enumerated() {
|
||||
let category = Category(context: context)
|
||||
category.id = stableID(for: categoryData.name)
|
||||
category.name = categoryData.name
|
||||
category.colorHex = categoryData.colorHex
|
||||
category.icon = categoryData.icon
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
/// Deterministic UUIDs (RFC 4122 v5) for records that every device/install must
|
||||
/// create identically — the default Account and the default Categories.
|
||||
///
|
||||
/// Why: `awakeFromInsert()` assigns a random UUID, so each device seeded its own
|
||||
/// "Default" account + default categories with *different* UUIDs before the first
|
||||
/// CloudKit import. CloudKit keys on the UUID (record name), so those identical-in-
|
||||
/// meaning records piled up as distinct records (8 accounts, 23 categories in the
|
||||
/// wild). Giving the defaults a STABLE UUID derived from a fixed namespace + name
|
||||
/// means every device produces the SAME record name → CloudKit natively merges
|
||||
/// them into one. No dedup, no deletes, no cascade — the root-cause fix.
|
||||
enum StableID {
|
||||
/// Fixed app namespace (generated once, constant forever — never change it).
|
||||
static let namespace = UUID(uuidString: "F3A1C2D4-5E6F-4A7B-8C9D-0E1F2A3B4C5D")!
|
||||
|
||||
/// RFC 4122 v5 UUID: SHA-1(namespace bytes ‖ name) with version/variant bits set.
|
||||
static func v5(_ name: String) -> UUID {
|
||||
var hasher = Insecure.SHA1()
|
||||
withUnsafeBytes(of: namespace.uuid) { hasher.update(bufferPointer: $0) }
|
||||
hasher.update(data: Data(name.utf8))
|
||||
let digest = Array(hasher.finalize()) // 20 bytes; take first 16
|
||||
|
||||
var b = Array(digest.prefix(16))
|
||||
b[6] = (b[6] & 0x0F) | 0x50 // version 5
|
||||
b[8] = (b[8] & 0x3F) | 0x80 // RFC 4122 variant
|
||||
|
||||
return UUID(uuid: (b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
|
||||
b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user