vOOice/VoiceInk/Services/DictionaryMigrationService.swift
Beingpax 60125c316b Migrate dictionary data from UserDefaults to SwiftData
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
2025-12-28 12:09:43 +05:45

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)
}
}