XCode-Claude-Workflow/SoliCards/Persistence/PersistenceManager.swift
idev2025 0f989f5c86 feat: SoliCards v1.2.0 — native SwiftUI solitaire for iOS, iPadOS, macOS
Complete native rewrite of the web-based SoliCards game as a SwiftUI
multiplatform app targeting iOS 17+, iPadOS 17+, and macOS 14+.

Three solitaire variants (Klondike, Spider, FreeCell) with full game
rules, drag & drop, smart zoom layout, 6 themes, 4 difficulty levels,
SwiftData persistence, VoiceOver accessibility, and 57 unit tests.

Key features:
- MVVM + Protocol-Oriented Strategy architecture
- DragGesture with coordinate-space hit-testing (long press + drag)
- Smart zoom: cards auto-size to fit screen based on deepest column
- Landscape: 30% bigger cards with scrollable overflow (iOS)
- macOS: 120pt card cap, 92% height buffer for window resizing
- Auto-save, game resume, statistics tracking via SwiftData
- Privacy manifest, app icon, String Catalog, zero dependencies

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 07:33:52 -04:00

110 lines
3.8 KiB
Swift

import Foundation
import SwiftData
@MainActor
final class PersistenceManager {
private let modelContext: ModelContext
init(modelContext: ModelContext) {
self.modelContext = modelContext
}
// MARK: - Game State
func saveGame(variant: GameVariant, difficulty: Difficulty, snapshot: GameSnapshot, elapsedSeconds: Int) {
// Delete any existing save for this variant
deleteSavedGame(for: variant)
let record = GameRecord(variant: variant, difficulty: difficulty,
snapshot: snapshot, elapsedSeconds: elapsedSeconds)
modelContext.insert(record)
try? modelContext.save()
}
func loadSavedGame(for variant: GameVariant) -> GameRecord? {
let variantRaw = variant.rawValue
let descriptor = FetchDescriptor<GameRecord>(
predicate: #Predicate { $0.variant == variantRaw },
sortBy: [SortDescriptor(\.lastPlayedDate, order: .reverse)]
)
return try? modelContext.fetch(descriptor).first
}
func loadMostRecentGame() -> GameRecord? {
let descriptor = FetchDescriptor<GameRecord>(
sortBy: [SortDescriptor(\.lastPlayedDate, order: .reverse)]
)
return try? modelContext.fetch(descriptor).first
}
func deleteSavedGame(for variant: GameVariant) {
let variantRaw = variant.rawValue
let descriptor = FetchDescriptor<GameRecord>(
predicate: #Predicate { $0.variant == variantRaw }
)
if let records = try? modelContext.fetch(descriptor) {
for record in records {
modelContext.delete(record)
}
}
}
// MARK: - Statistics
func recordWin(variant: GameVariant, difficulty: Difficulty, score: Int, time: Int) {
let record = fetchOrCreateStats(variant: variant, difficulty: difficulty)
record.recordWin(score: score, time: time)
try? modelContext.save()
}
func recordLoss(variant: GameVariant, difficulty: Difficulty) {
let record = fetchOrCreateStats(variant: variant, difficulty: difficulty)
record.recordLoss()
try? modelContext.save()
}
func fetchStats(variant: GameVariant, difficulty: Difficulty) -> StatsRecord? {
let variantRaw = variant.rawValue
let difficultyRaw = difficulty.rawValue
let descriptor = FetchDescriptor<StatsRecord>(
predicate: #Predicate { $0.variant == variantRaw && $0.difficulty == difficultyRaw }
)
return try? modelContext.fetch(descriptor).first
}
private func fetchOrCreateStats(variant: GameVariant, difficulty: Difficulty) -> StatsRecord {
if let existing = fetchStats(variant: variant, difficulty: difficulty) {
return existing
}
let record = StatsRecord(variant: variant, difficulty: difficulty)
modelContext.insert(record)
return record
}
// MARK: - Preferences
func loadPreferences() -> PrefsRecord {
let descriptor = FetchDescriptor<PrefsRecord>()
if let existing = try? modelContext.fetch(descriptor).first {
return existing
}
let record = PrefsRecord()
modelContext.insert(record)
try? modelContext.save()
return record
}
func savePreferences(themeId: String, cardFaceStyle: CardFaceStyle,
cardBackDesign: CardBackDesign, soundEnabled: Bool,
lastVariant: GameVariant, lastDifficulty: Difficulty) {
let prefs = loadPreferences()
prefs.themeId = themeId
prefs.cardFaceStyle = cardFaceStyle.rawValue
prefs.cardBackDesign = cardBackDesign.rawValue
prefs.soundEnabled = soundEnabled
prefs.lastVariant = lastVariant.rawValue
prefs.lastDifficulty = lastDifficulty.rawValue
try? modelContext.save()
}
}