Implement storage architecture from ai_blueprint.md
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>
This commit is contained in:
70
IYmtg_App_iOS/Services/Cloud/CloudEngine.swift
Normal file
70
IYmtg_App_iOS/Services/Cloud/CloudEngine.swift
Normal file
@@ -0,0 +1,70 @@
|
||||
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)") }
|
||||
}
|
||||
}
|
||||
}
|
||||
13
IYmtg_App_iOS/Services/Cloud/TrainingUploader.swift
Normal file
13
IYmtg_App_iOS/Services/Cloud/TrainingUploader.swift
Normal file
@@ -0,0 +1,13 @@
|
||||
import UIKit
|
||||
import FirebaseStorage
|
||||
|
||||
// MARK: - TRAINING UPLOADER
|
||||
class TrainingUploader {
|
||||
static func upload(image: UIImage, label: String, force: Bool = false) {
|
||||
if !force && !AppConfig.isTrainingOptIn { return }
|
||||
guard FirebaseApp.app() != nil else { return }
|
||||
let safeLabel = label.replacingOccurrences(of: "/", with: "-")
|
||||
let ref = Storage.storage().reference().child("training/\(safeLabel)/\(UUID().uuidString).jpg")
|
||||
if let data = image.jpegData(compressionQuality: 0.9) { ref.putData(data, metadata: nil) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user