fix CloudKit compatibility: default values, optional relationship, background mode
- Add default values to all SwiftData attributes (CloudKit requirement) - Make memos relationship optional (CloudKit requirement) - Add allMemos computed property for safe unwrapping - Add remote-notification background mode for CloudKit push sync Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -291,6 +291,7 @@
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Voice Diary needs microphone access to record your voice memos for diary entries.";
|
||||
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "Voice Diary uses on-device speech recognition to transcribe your voice memos.";
|
||||
INFOPLIST_KEY_UIBackgroundModes = "remote-notification";
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
@@ -374,6 +375,7 @@
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Voice Diary needs microphone access to record your voice memos for diary entries.";
|
||||
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "Voice Diary uses on-device speech recognition to transcribe your voice memos.";
|
||||
INFOPLIST_KEY_UIBackgroundModes = "remote-notification";
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
|
||||
@@ -3,13 +3,13 @@ import SwiftData
|
||||
|
||||
@Model
|
||||
final class DiaryEntry {
|
||||
var date: Date
|
||||
var date: Date = Date.now
|
||||
var summary: String?
|
||||
var isSummaryGenerated: Bool
|
||||
var isSummaryGenerated: Bool = false
|
||||
@Relationship(deleteRule: .cascade, inverse: \VoiceMemo.entry)
|
||||
var memos: [VoiceMemo]
|
||||
var createdAt: Date
|
||||
var updatedAt: Date
|
||||
var memos: [VoiceMemo]? = []
|
||||
var createdAt: Date = Date.now
|
||||
var updatedAt: Date = Date.now
|
||||
|
||||
init(date: Date) {
|
||||
self.date = Calendar.current.startOfDay(for: date)
|
||||
@@ -20,19 +20,23 @@ final class DiaryEntry {
|
||||
self.updatedAt = .now
|
||||
}
|
||||
|
||||
var allMemos: [VoiceMemo] {
|
||||
memos ?? []
|
||||
}
|
||||
|
||||
var combinedTranscript: String {
|
||||
memos
|
||||
allMemos
|
||||
.sorted { $0.recordedAt < $1.recordedAt }
|
||||
.compactMap(\.transcript)
|
||||
.joined(separator: "\n\n")
|
||||
}
|
||||
|
||||
var hasMemos: Bool {
|
||||
!memos.isEmpty
|
||||
!allMemos.isEmpty
|
||||
}
|
||||
|
||||
var hasTranscripts: Bool {
|
||||
memos.contains { $0.transcript != nil }
|
||||
allMemos.contains { $0.transcript != nil }
|
||||
}
|
||||
|
||||
var formattedDate: String {
|
||||
|
||||
@@ -3,11 +3,11 @@ import SwiftData
|
||||
|
||||
@Model
|
||||
final class VoiceMemo {
|
||||
var audioFileName: String
|
||||
var audioFileName: String = ""
|
||||
var transcript: String?
|
||||
var isTranscribing: Bool
|
||||
var duration: TimeInterval
|
||||
var recordedAt: Date
|
||||
var isTranscribing: Bool = false
|
||||
var duration: TimeInterval = 0
|
||||
var recordedAt: Date = Date.now
|
||||
var entry: DiaryEntry?
|
||||
|
||||
init(audioFileName: String, duration: TimeInterval) {
|
||||
|
||||
@@ -27,7 +27,7 @@ final class DiaryViewModel {
|
||||
|
||||
func deleteEntry(_ entry: DiaryEntry, context: ModelContext) {
|
||||
// Delete audio files
|
||||
for memo in entry.memos {
|
||||
for memo in entry.allMemos {
|
||||
try? FileManager.default.removeItem(at: memo.audioURL)
|
||||
}
|
||||
context.delete(entry)
|
||||
@@ -35,7 +35,7 @@ final class DiaryViewModel {
|
||||
|
||||
func deleteMemo(_ memo: VoiceMemo, from entry: DiaryEntry, context: ModelContext) {
|
||||
try? FileManager.default.removeItem(at: memo.audioURL)
|
||||
entry.memos.removeAll { $0.persistentModelID == memo.persistentModelID }
|
||||
entry.memos?.removeAll { $0.persistentModelID == memo.persistentModelID }
|
||||
context.delete(memo)
|
||||
entry.updatedAt = .now
|
||||
}
|
||||
|
||||
@@ -42,7 +42,8 @@ final class RecordingViewModel {
|
||||
)
|
||||
context.insert(memo)
|
||||
memo.entry = entry
|
||||
entry.memos.append(memo)
|
||||
if entry.memos == nil { entry.memos = [] }
|
||||
entry.memos?.append(memo)
|
||||
entry.updatedAt = .now
|
||||
|
||||
try? context.save()
|
||||
|
||||
@@ -80,7 +80,7 @@ struct DiaryListRow: View {
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Label(
|
||||
String(localized: "diary.memoCount \(entry.memos.count)"),
|
||||
String(localized: "diary.memoCount \(entry.allMemos.count)"),
|
||||
systemImage: "waveform"
|
||||
)
|
||||
|
||||
|
||||
@@ -96,11 +96,11 @@ struct DiaryEntryView: View {
|
||||
|
||||
private var memosTab: some View {
|
||||
List {
|
||||
ForEach(entry.memos.sorted(by: { $0.recordedAt < $1.recordedAt })) { memo in
|
||||
ForEach(entry.allMemos.sorted(by: { $0.recordedAt < $1.recordedAt })) { memo in
|
||||
MemoRow(memo: memo)
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
let sorted = entry.memos.sorted { $0.recordedAt < $1.recordedAt }
|
||||
let sorted = entry.allMemos.sorted { $0.recordedAt < $1.recordedAt }
|
||||
for index in indexSet {
|
||||
viewModel.deleteMemo(sorted[index], from: entry, context: modelContext)
|
||||
}
|
||||
@@ -108,7 +108,7 @@ struct DiaryEntryView: View {
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.overlay {
|
||||
if entry.memos.isEmpty {
|
||||
if entry.allMemos.isEmpty {
|
||||
ContentUnavailableView(
|
||||
String(localized: "memos.empty.title"),
|
||||
systemImage: "mic.slash",
|
||||
|
||||
@@ -23,6 +23,7 @@ struct DiaryEntryTests {
|
||||
|
||||
entry.memos = [memo1, memo2]
|
||||
|
||||
#expect(entry.allMemos.count == 2)
|
||||
#expect(entry.combinedTranscript.contains("First memo."))
|
||||
#expect(entry.combinedTranscript.contains("Second memo."))
|
||||
}
|
||||
@@ -38,7 +39,7 @@ struct DiaryEntryTests {
|
||||
let entry = DiaryEntry(date: .now)
|
||||
#expect(!entry.hasMemos)
|
||||
|
||||
entry.memos.append(VoiceMemo(audioFileName: "test.m4a", duration: 5))
|
||||
entry.memos = [VoiceMemo(audioFileName: "test.m4a", duration: 5)]
|
||||
#expect(entry.hasMemos)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ targets:
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad: "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription: "Voice Diary needs microphone access to record your voice memos for diary entries."
|
||||
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription: "Voice Diary uses on-device speech recognition to transcribe your voice memos."
|
||||
INFOPLIST_KEY_UIBackgroundModes: remote-notification
|
||||
entitlements:
|
||||
path: VoiceDiary/VoiceDiary.entitlements
|
||||
properties:
|
||||
|
||||
Reference in New Issue
Block a user