Primary sync: replace PersistenceActor JSON file with SwiftData + CloudKit - Add SavedCardModel (@Model class) and PersistenceController (ModelContainer with .automatic CloudKit, fallback to local). BackgroundPersistenceActor (@ModelActor) handles all DB I/O off the main thread. - One-time migration imports user_collection.json into SwiftData and renames the original file to prevent re-import. - Inject modelContainer into SwiftUI environment in IYmtgApp. Image storage: Documents/UserContent/ subfolder (blueprint requirement) - ImageManager.dir now targets iCloud Documents/UserContent/ (or local equiv). - migrateImagesToUserContent() moves existing JPGs to the new subfolder on first launch; called during the SwiftData migration. Firebase: demoted to optional manual backup (metadata only, no images) - Remove all automatic CloudEngine.save/delete/batchUpdatePrices calls from CollectionViewModel mutations. - Add backupAllToFirebase() for user-triggered metadata sync. - Add isFirebaseBackupEnabled to AppConfig (default false). - Add Cloud Backup section in Library settings with iCloud vs Firebase explanation and "Backup Metadata to Firebase Now" button. Also: full modular refactor (Data/, Features/, Services/ directories) and README updated with CloudKit setup steps and revised release checklist. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
71 lines
2.7 KiB
Swift
71 lines
2.7 KiB
Swift
import FirebaseFirestore
|
|
import FirebaseAuth
|
|
|
|
// MARK: - CLOUD ENGINE
|
|
class CloudEngine {
|
|
static var db: Firestore? {
|
|
return FirebaseApp.app() != nil ? Firestore.firestore() : nil
|
|
}
|
|
|
|
static func signInSilently() {
|
|
guard FirebaseApp.app() != nil else { return }
|
|
if Auth.auth().currentUser == nil { Auth.auth().signInAnonymously() }
|
|
}
|
|
|
|
static func save(card: SavedCard) async {
|
|
guard let db = db, let uid = Auth.auth().currentUser?.uid else { return }
|
|
let data: [String: Any] = [
|
|
"name": card.name,
|
|
"set": card.setCode,
|
|
"num": card.collectorNumber,
|
|
"val": card.currentValuation ?? 0.0,
|
|
"cond": card.condition,
|
|
"foil": card.foilType,
|
|
"coll": card.collectionName,
|
|
"loc": card.storageLocation,
|
|
"grade": card.grade ?? "",
|
|
"svc": card.gradingService ?? "",
|
|
"cert": card.certNumber ?? "",
|
|
"date": card.dateAdded,
|
|
"curr": card.currencyCode ?? "USD",
|
|
"rarity": card.rarity ?? "",
|
|
"colors": card.colorIdentity ?? [],
|
|
"ser": card.isSerialized ?? false,
|
|
"img": card.imageFileName,
|
|
"custom": card.isCustomValuation
|
|
]
|
|
do {
|
|
try await db.collection("users").document(uid).collection("inventory").document(card.id.uuidString).setData(data, merge: true)
|
|
} catch { print("Cloud Save Error: \(error)") }
|
|
}
|
|
|
|
static func delete(card: SavedCard) async {
|
|
guard let db = db, let uid = Auth.auth().currentUser?.uid else { return }
|
|
do {
|
|
try await db.collection("users").document(uid).collection("inventory").document(card.id.uuidString).delete()
|
|
} catch { print("Cloud Delete Error: \(error)") }
|
|
}
|
|
|
|
static func batchUpdatePrices(cards: [SavedCard]) async {
|
|
guard let db = db, let uid = Auth.auth().currentUser?.uid else { return }
|
|
let ref = db.collection("users").document(uid).collection("inventory")
|
|
|
|
let chunks = stride(from: 0, to: cards.count, by: 400).map { Array(cards[$0..<min($0 + 400, cards.count)]) }
|
|
for chunk in chunks {
|
|
let batch = db.batch()
|
|
for card in chunk {
|
|
let data: [String: Any] = [
|
|
"val": card.currentValuation ?? 0.0,
|
|
"curr": card.currencyCode ?? "USD",
|
|
"rarity": card.rarity ?? "",
|
|
"colors": card.colorIdentity ?? []
|
|
]
|
|
batch.setData(data, forDocument: ref.document(card.id.uuidString), merge: true)
|
|
}
|
|
do {
|
|
try await batch.commit()
|
|
} catch { print("Batch Update Error: \(error)") }
|
|
}
|
|
}
|
|
}
|