Migrates vocabulary words and word replacements from UserDefaults to SwiftData for better data management and persistence. Changes: - Create VocabularyWord and WordReplacement SwiftData models - Add dual ModelConfiguration setup (default.store for transcripts, dictionary.store for dictionary data) - Implement DictionaryMigrationService for one-time UserDefaults→SwiftData migration - Rename "Correct Spellings" to "Vocabulary" for clearer terminology - Update all dictionary views to use @Query instead of manager classes - Update all services to fetch from SwiftData using FetchDescriptor - Enhance word replacement duplicate detection (now checks during add AND edit) - Update import/export services to work with SwiftData - Preserve all existing functionality with improved data integrity Technical details: - Separate store files: default.store (transcripts) + dictionary.store (vocabulary + replacements) - Migration flag: "HasMigratedDictionaryToSwiftData_v2" - All CRUD operations properly implemented with duplicate detection
100 lines
3.7 KiB
Swift
100 lines
3.7 KiB
Swift
import Foundation
|
|
import SwiftData
|
|
import OSLog
|
|
|
|
class DictionaryMigrationService {
|
|
static let shared = DictionaryMigrationService()
|
|
private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "DictionaryMigration")
|
|
|
|
private let migrationCompletedKey = "HasMigratedDictionaryToSwiftData_v2"
|
|
private let vocabularyKey = "CustomVocabularyItems"
|
|
private let wordReplacementsKey = "wordReplacements"
|
|
|
|
private init() {}
|
|
|
|
/// Migrates dictionary data from UserDefaults to SwiftData
|
|
/// This is a one-time operation that preserves all existing user data
|
|
func migrateIfNeeded(context: ModelContext) {
|
|
// Check if migration has already been completed
|
|
if UserDefaults.standard.bool(forKey: migrationCompletedKey) {
|
|
logger.info("Dictionary migration already completed, skipping")
|
|
return
|
|
}
|
|
|
|
logger.info("Starting dictionary migration from UserDefaults to SwiftData")
|
|
|
|
var vocabularyMigrated = 0
|
|
var replacementsMigrated = 0
|
|
|
|
// Migrate vocabulary words
|
|
if let data = UserDefaults.standard.data(forKey: vocabularyKey) {
|
|
do {
|
|
// Decode old vocabulary structure
|
|
let decoder = JSONDecoder()
|
|
let oldVocabulary = try decoder.decode([OldVocabularyWord].self, from: data)
|
|
|
|
logger.info("Found \(oldVocabulary.count) vocabulary words to migrate")
|
|
|
|
for oldWord in oldVocabulary {
|
|
let newWord = VocabularyWord(word: oldWord.word)
|
|
context.insert(newWord)
|
|
vocabularyMigrated += 1
|
|
}
|
|
|
|
logger.info("Successfully migrated \(vocabularyMigrated) vocabulary words")
|
|
} catch {
|
|
logger.error("Failed to migrate vocabulary words: \(error.localizedDescription)")
|
|
}
|
|
} else {
|
|
logger.info("No vocabulary words found to migrate")
|
|
}
|
|
|
|
// Migrate word replacements
|
|
if let replacements = UserDefaults.standard.dictionary(forKey: wordReplacementsKey) as? [String: String] {
|
|
logger.info("Found \(replacements.count) word replacements to migrate")
|
|
|
|
for (originalText, replacementText) in replacements {
|
|
let wordReplacement = WordReplacement(
|
|
originalText: originalText,
|
|
replacementText: replacementText
|
|
)
|
|
context.insert(wordReplacement)
|
|
replacementsMigrated += 1
|
|
}
|
|
|
|
logger.info("Successfully migrated \(replacementsMigrated) word replacements")
|
|
} else {
|
|
logger.info("No word replacements found to migrate")
|
|
}
|
|
|
|
// Save the migrated data
|
|
do {
|
|
try context.save()
|
|
logger.info("Successfully saved migrated data to SwiftData")
|
|
|
|
// Mark migration as completed
|
|
UserDefaults.standard.set(true, forKey: migrationCompletedKey)
|
|
logger.info("Migration completed successfully")
|
|
} catch {
|
|
logger.error("Failed to save migrated data: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Legacy structure for decoding old vocabulary data
|
|
private struct OldVocabularyWord: Decodable {
|
|
let word: String
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case id, word, dateAdded
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
word = try container.decode(String.self, forKey: .word)
|
|
// Ignore other fields that may exist in old format
|
|
_ = try? container.decodeIfPresent(UUID.self, forKey: .id)
|
|
_ = try? container.decodeIfPresent(Date.self, forKey: .dateAdded)
|
|
}
|
|
}
|