vOOice/VoiceInk/Views/Dictionary/EditReplacementSheet.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

166 lines
5.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
import SwiftData
// Edit existing word replacement entry
struct EditReplacementSheet: View {
let replacement: WordReplacement
let modelContext: ModelContext
@Environment(\.dismiss) private var dismiss
@State private var originalWord: String
@State private var replacementWord: String
@State private var showAlert = false
@State private var alertMessage = ""
// MARK: Initialiser
init(replacement: WordReplacement, modelContext: ModelContext) {
self.replacement = replacement
self.modelContext = modelContext
_originalWord = State(initialValue: replacement.originalText)
_replacementWord = State(initialValue: replacement.replacementText)
}
var body: some View {
VStack(spacing: 0) {
header
Divider()
formContent
}
.frame(width: 460, height: 560)
.alert("Word Replacement", isPresented: $showAlert) {
Button("OK", role: .cancel) {}
} message: {
Text(alertMessage)
}
}
// MARK: Subviews
private var header: some View {
HStack {
Button("Cancel", role: .cancel) { dismiss() }
.buttonStyle(.borderless)
.keyboardShortcut(.escape, modifiers: [])
Spacer()
Text("Edit Word Replacement")
.font(.headline)
Spacer()
Button("Save") { saveChanges() }
.buttonStyle(.borderedProminent)
.controlSize(.small)
.disabled(originalWord.isEmpty || replacementWord.isEmpty)
.keyboardShortcut(.return, modifiers: [])
}
.padding(.horizontal)
.padding(.vertical, 12)
.background(CardBackground(isSelected: false))
}
private var formContent: some View {
ScrollView {
VStack(spacing: 20) {
descriptionSection
inputSection
}
.padding(.vertical)
}
}
private var descriptionSection: some View {
Text("Update the word or phrase that should be automatically replaced.")
.font(.subheadline)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
.padding(.top, 8)
}
private var inputSection: some View {
VStack(spacing: 16) {
// Original Text Field
VStack(alignment: .leading, spacing: 6) {
HStack {
Text("Original Text")
.font(.headline)
Text("Required")
.font(.caption)
.foregroundColor(.secondary)
}
TextField("Enter word or phrase to replace (use commas for multiple)", text: $originalWord)
.textFieldStyle(.roundedBorder)
}
.padding(.horizontal)
// Replacement Text Field
VStack(alignment: .leading, spacing: 6) {
HStack {
Text("Replacement Text")
.font(.headline)
Text("Required")
.font(.caption)
.foregroundColor(.secondary)
}
TextEditor(text: $replacementWord)
.font(.body)
.frame(height: 100)
.padding(8)
.background(Color(.textBackgroundColor))
.cornerRadius(6)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(Color(.separatorColor), lineWidth: 1)
)
}
.padding(.horizontal)
}
}
// MARK: Actions
private func saveChanges() {
let newOriginal = originalWord.trimmingCharacters(in: .whitespacesAndNewlines)
let newReplacement = replacementWord
let tokens = newOriginal
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
guard !tokens.isEmpty, !newReplacement.isEmpty else { return }
// Check for duplicates (excluding current replacement)
let newTokensPairs = tokens.map { (original: $0, lowercased: $0.lowercased()) }
let descriptor = FetchDescriptor<WordReplacement>()
if let allReplacements = try? modelContext.fetch(descriptor) {
for existingReplacement in allReplacements {
// Skip checking against itself
if existingReplacement.id == replacement.id {
continue
}
let existingTokens = existingReplacement.originalText
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() }
.filter { !$0.isEmpty }
for tokenPair in newTokensPairs {
if existingTokens.contains(tokenPair.lowercased) {
alertMessage = "'\(tokenPair.original)' already exists in word replacements"
showAlert = true
return
}
}
}
}
// Update the replacement
replacement.originalText = newOriginal
replacement.replacementText = newReplacement
try? modelContext.save()
dismiss()
}
}