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:
109
IYmtg_App_iOS/Data/Models/Card.swift
Normal file
109
IYmtg_App_iOS/Data/Models/Card.swift
Normal file
@@ -0,0 +1,109 @@
|
||||
import Foundation
|
||||
|
||||
struct CardFingerprint: Codable, Identifiable, Sendable {
|
||||
let id: UUID
|
||||
let name: String
|
||||
let setCode: String
|
||||
let collectorNumber: String
|
||||
let hasFoilPrinting: Bool
|
||||
let hasSerializedPrinting: Bool?
|
||||
let featureData: Data
|
||||
var priceScanned: Double? = nil
|
||||
}
|
||||
|
||||
struct CardMetadata: Identifiable, Sendable {
|
||||
let id: UUID
|
||||
let name: String
|
||||
let setCode: String
|
||||
let collectorNumber: String
|
||||
let hasFoilPrinting: Bool
|
||||
let hasSerializedPrinting: Bool
|
||||
var priceScanned: Double? = nil
|
||||
var rarity: String? = nil
|
||||
var colorIdentity: [String]? = nil
|
||||
var isSerialized: Bool = false
|
||||
}
|
||||
|
||||
struct SavedCard: Codable, Identifiable, Hashable, Sendable {
|
||||
let id: UUID
|
||||
let scryfallID: String
|
||||
var name: String
|
||||
var setCode: String
|
||||
var collectorNumber: String
|
||||
let imageFileName: String
|
||||
var condition: String
|
||||
var foilType: String
|
||||
var currentValuation: Double?
|
||||
var previousValuation: Double?
|
||||
var dateAdded: Date
|
||||
var classification: String
|
||||
var collectionName: String
|
||||
var storageLocation: String
|
||||
var rarity: String?
|
||||
var colorIdentity: [String]?
|
||||
|
||||
// Grading Fields
|
||||
var gradingService: String? // PSA, BGS
|
||||
var grade: String? // 10, 9.5
|
||||
var certNumber: String? // 123456
|
||||
var isCustomValuation: Bool = false
|
||||
var isSerialized: Bool? = false
|
||||
var currencyCode: String?
|
||||
|
||||
init(from scan: CardMetadata, imageName: String, collection: String, location: String) {
|
||||
self.id = UUID()
|
||||
self.scryfallID = "\(scan.setCode)-\(scan.collectorNumber)"
|
||||
self.name = scan.name
|
||||
self.setCode = scan.setCode
|
||||
self.collectorNumber = scan.collectorNumber
|
||||
self.imageFileName = imageName
|
||||
self.condition = AppConfig.Defaults.defaultCondition
|
||||
self.foilType = AppConfig.Defaults.defaultFoil
|
||||
self.currentValuation = scan.priceScanned
|
||||
self.previousValuation = scan.priceScanned
|
||||
self.dateAdded = Date()
|
||||
self.classification = "Unknown"
|
||||
self.collectionName = collection
|
||||
self.storageLocation = location
|
||||
self.rarity = scan.rarity
|
||||
self.colorIdentity = scan.colorIdentity
|
||||
self.isSerialized = scan.isSerialized
|
||||
}
|
||||
|
||||
/// Full memberwise init used by SavedCardModel (SwiftData) ↔ SavedCard conversion.
|
||||
init(id: UUID, scryfallID: String, name: String, setCode: String, collectorNumber: String,
|
||||
imageFileName: String, condition: String, foilType: String, currentValuation: Double?,
|
||||
previousValuation: Double?, dateAdded: Date, classification: String, collectionName: String,
|
||||
storageLocation: String, rarity: String?, colorIdentity: [String]?,
|
||||
gradingService: String?, grade: String?, certNumber: String?,
|
||||
isCustomValuation: Bool, isSerialized: Bool?, currencyCode: String?) {
|
||||
self.id = id
|
||||
self.scryfallID = scryfallID
|
||||
self.name = name
|
||||
self.setCode = setCode
|
||||
self.collectorNumber = collectorNumber
|
||||
self.imageFileName = imageFileName
|
||||
self.condition = condition
|
||||
self.foilType = foilType
|
||||
self.currentValuation = currentValuation
|
||||
self.previousValuation = previousValuation
|
||||
self.dateAdded = dateAdded
|
||||
self.classification = classification
|
||||
self.collectionName = collectionName
|
||||
self.storageLocation = storageLocation
|
||||
self.rarity = rarity
|
||||
self.colorIdentity = colorIdentity
|
||||
self.gradingService = gradingService
|
||||
self.grade = grade
|
||||
self.certNumber = certNumber
|
||||
self.isCustomValuation = isCustomValuation
|
||||
self.isSerialized = isSerialized
|
||||
self.currencyCode = currencyCode
|
||||
}
|
||||
}
|
||||
|
||||
enum MatchResult {
|
||||
case exact(CardMetadata)
|
||||
case ambiguous(name: String, candidates: [CardMetadata])
|
||||
case unknown
|
||||
}
|
||||
156
IYmtg_App_iOS/Data/Models/SavedCardModel.swift
Normal file
156
IYmtg_App_iOS/Data/Models/SavedCardModel.swift
Normal file
@@ -0,0 +1,156 @@
|
||||
import SwiftData
|
||||
import Foundation
|
||||
|
||||
// MARK: - SAVED CARD MODEL
|
||||
// SwiftData @Model class for structured persistence with automatic CloudKit sync.
|
||||
// Mirrors the SavedCard value-type struct so the rest of the app stays unchanged.
|
||||
// Requires iOS 17+. Set minimum deployment target to iOS 17 in Xcode.
|
||||
|
||||
@Model
|
||||
final class SavedCardModel {
|
||||
@Attribute(.unique) var id: UUID
|
||||
var scryfallID: String
|
||||
var name: String
|
||||
var setCode: String
|
||||
var collectorNumber: String
|
||||
var imageFileName: String
|
||||
var condition: String
|
||||
var foilType: String
|
||||
var currentValuation: Double?
|
||||
var previousValuation: Double?
|
||||
var dateAdded: Date
|
||||
var classification: String
|
||||
var collectionName: String
|
||||
var storageLocation: String
|
||||
var rarity: String?
|
||||
var colorIdentity: [String]?
|
||||
var gradingService: String?
|
||||
var grade: String?
|
||||
var certNumber: String?
|
||||
var isCustomValuation: Bool
|
||||
var isSerialized: Bool?
|
||||
var currencyCode: String?
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
scryfallID: String,
|
||||
name: String,
|
||||
setCode: String,
|
||||
collectorNumber: String,
|
||||
imageFileName: String,
|
||||
condition: String,
|
||||
foilType: String,
|
||||
currentValuation: Double? = nil,
|
||||
previousValuation: Double? = nil,
|
||||
dateAdded: Date = Date(),
|
||||
classification: String = "Unknown",
|
||||
collectionName: String,
|
||||
storageLocation: String,
|
||||
rarity: String? = nil,
|
||||
colorIdentity: [String]? = nil,
|
||||
gradingService: String? = nil,
|
||||
grade: String? = nil,
|
||||
certNumber: String? = nil,
|
||||
isCustomValuation: Bool = false,
|
||||
isSerialized: Bool? = false,
|
||||
currencyCode: String? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.scryfallID = scryfallID
|
||||
self.name = name
|
||||
self.setCode = setCode
|
||||
self.collectorNumber = collectorNumber
|
||||
self.imageFileName = imageFileName
|
||||
self.condition = condition
|
||||
self.foilType = foilType
|
||||
self.currentValuation = currentValuation
|
||||
self.previousValuation = previousValuation
|
||||
self.dateAdded = dateAdded
|
||||
self.classification = classification
|
||||
self.collectionName = collectionName
|
||||
self.storageLocation = storageLocation
|
||||
self.rarity = rarity
|
||||
self.colorIdentity = colorIdentity
|
||||
self.gradingService = gradingService
|
||||
self.grade = grade
|
||||
self.certNumber = certNumber
|
||||
self.isCustomValuation = isCustomValuation
|
||||
self.isSerialized = isSerialized
|
||||
self.currencyCode = currencyCode
|
||||
}
|
||||
|
||||
convenience init(from card: SavedCard) {
|
||||
self.init(
|
||||
id: card.id,
|
||||
scryfallID: card.scryfallID,
|
||||
name: card.name,
|
||||
setCode: card.setCode,
|
||||
collectorNumber: card.collectorNumber,
|
||||
imageFileName: card.imageFileName,
|
||||
condition: card.condition,
|
||||
foilType: card.foilType,
|
||||
currentValuation: card.currentValuation,
|
||||
previousValuation: card.previousValuation,
|
||||
dateAdded: card.dateAdded,
|
||||
classification: card.classification,
|
||||
collectionName: card.collectionName,
|
||||
storageLocation: card.storageLocation,
|
||||
rarity: card.rarity,
|
||||
colorIdentity: card.colorIdentity,
|
||||
gradingService: card.gradingService,
|
||||
grade: card.grade,
|
||||
certNumber: card.certNumber,
|
||||
isCustomValuation: card.isCustomValuation,
|
||||
isSerialized: card.isSerialized,
|
||||
currencyCode: card.currencyCode
|
||||
)
|
||||
}
|
||||
|
||||
func update(from card: SavedCard) {
|
||||
name = card.name
|
||||
setCode = card.setCode
|
||||
collectorNumber = card.collectorNumber
|
||||
condition = card.condition
|
||||
foilType = card.foilType
|
||||
currentValuation = card.currentValuation
|
||||
previousValuation = card.previousValuation
|
||||
classification = card.classification
|
||||
collectionName = card.collectionName
|
||||
storageLocation = card.storageLocation
|
||||
rarity = card.rarity
|
||||
colorIdentity = card.colorIdentity
|
||||
gradingService = card.gradingService
|
||||
grade = card.grade
|
||||
certNumber = card.certNumber
|
||||
isCustomValuation = card.isCustomValuation
|
||||
isSerialized = card.isSerialized
|
||||
currencyCode = card.currencyCode
|
||||
}
|
||||
|
||||
func toSavedCard() -> SavedCard {
|
||||
SavedCard(
|
||||
id: id,
|
||||
scryfallID: scryfallID,
|
||||
name: name,
|
||||
setCode: setCode,
|
||||
collectorNumber: collectorNumber,
|
||||
imageFileName: imageFileName,
|
||||
condition: condition,
|
||||
foilType: foilType,
|
||||
currentValuation: currentValuation,
|
||||
previousValuation: previousValuation,
|
||||
dateAdded: dateAdded,
|
||||
classification: classification,
|
||||
collectionName: collectionName,
|
||||
storageLocation: storageLocation,
|
||||
rarity: rarity,
|
||||
colorIdentity: colorIdentity,
|
||||
gradingService: gradingService,
|
||||
grade: grade,
|
||||
certNumber: certNumber,
|
||||
isCustomValuation: isCustomValuation,
|
||||
isSerialized: isSerialized,
|
||||
currencyCode: currencyCode
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user