Files
voicediary/VoiceDiary/Views/ContentView.swift
Felix Förtsch 3d42c83f75 fix speech authorization crash: decouple from SwiftData context
- Make TranscriptionService a plain Sendable class (not @Observable/@MainActor)
- Request speech authorization in ContentView.onAppear via callback (no async)
- Use @State pendingMemo + Task in View for transcription (Swift 6 safe)
- Separate saveRecording() and startTranscription() to avoid data races

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:31:59 +01:00

120 lines
2.7 KiB
Swift

import SwiftData
import SwiftUI
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query(sort: \DiaryEntry.date, order: .reverse) private var entries: [DiaryEntry]
@State private var recordingViewModel = RecordingViewModel()
@State private var diaryViewModel = DiaryViewModel()
@State private var showingRecording = false
@State private var speechAuthorized = false
var body: some View {
NavigationStack {
Group {
if entries.isEmpty {
emptyState
} else {
diaryList
}
}
.navigationTitle(String(localized: "diary.title"))
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
showingRecording = true
} label: {
Image(systemName: "mic.circle.fill")
.font(.title2)
}
.accessibilityLabel(String(localized: "recording.start"))
}
}
.sheet(isPresented: $showingRecording) {
RecordingView(viewModel: recordingViewModel)
}
}
.environment(diaryViewModel)
.onAppear {
SpeechAuthorization.requestIfNeeded { authorized in
speechAuthorized = authorized
}
}
}
private var emptyState: some View {
ContentUnavailableView {
Label(String(localized: "diary.empty.title"), systemImage: "book.closed")
} description: {
Text(String(localized: "diary.empty.description"))
} actions: {
Button {
showingRecording = true
} label: {
Text(String(localized: "diary.empty.action"))
}
.buttonStyle(.borderedProminent)
}
}
private var diaryList: some View {
List {
ForEach(entries) { entry in
NavigationLink(value: entry) {
DiaryListRow(entry: entry)
}
}
.onDelete { indexSet in
for index in indexSet {
diaryViewModel.deleteEntry(entries[index], context: modelContext)
}
}
}
.navigationDestination(for: DiaryEntry.self) { entry in
DiaryEntryView(entry: entry)
}
}
}
struct DiaryListRow: View {
let entry: DiaryEntry
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(entry.formattedDate)
.font(.headline)
HStack(spacing: 12) {
Label(
String(localized: "diary.memoCount \(entry.allMemos.count)"),
systemImage: "waveform"
)
if entry.isSummaryGenerated {
Label(
String(localized: "diary.summarized"),
systemImage: "checkmark.circle"
)
.foregroundStyle(.green)
}
}
.font(.caption)
.foregroundStyle(.secondary)
if let summary = entry.summary {
Text(summary.prefix(120))
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(2)
}
}
.padding(.vertical, 4)
.accessibilityElement(children: .combine)
}
}
#Preview {
ContentView()
.modelContainer(for: DiaryEntry.self, inMemory: true)
}